From 17287cef33c4ac77ff7089f9f3a9eab6c59b6516 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Tue, 4 Apr 2023 13:39:41 -0400 Subject: [PATCH] Add an OTA download test to Darwin framework CI. (#25951) This tests that we can run all the way through an OTA update that takes multiple BDX blocks to transfer, and that the right data is received on the requestor side. --- .github/workflows/darwin.yaml | 4 +- .../Framework/CHIPTests/MTROTAProviderTests.m | 224 +++++++++++++++++- 2 files changed, 216 insertions(+), 12 deletions(-) diff --git a/.github/workflows/darwin.yaml b/.github/workflows/darwin.yaml index 04d23b3f3c5075..6c82a17f224bf9 100644 --- a/.github/workflows/darwin.yaml +++ b/.github/workflows/darwin.yaml @@ -143,8 +143,8 @@ jobs: # all-clusters-app and from other requestors. # # And a different port from the test harness too; the test harness uses port 5541. - ../../../out/debug/chip-ota-requestor-app --interface-id -1 --secured-device-port 5542 --discriminator 1111 --KVS /tmp/chip-ota-requestor-kvs1 > >(tee /tmp/darwin/framework-tests/ota-requestor-app-1.log) 2> >(tee /tmp/darwin/framework-tests/ota-requestor-app-err-1.log >&2) & - ../../../out/debug/chip-ota-requestor-app --interface-id -1 --secured-device-port 5543 --discriminator 1112 --KVS /tmp/chip-ota-requestor-kvs2 > >(tee /tmp/darwin/framework-tests/ota-requestor-app-2.log) 2> >(tee /tmp/darwin/framework-tests/ota-requestor-app-err-2.log >&2) & + ../../../out/debug/chip-ota-requestor-app --interface-id -1 --secured-device-port 5542 --discriminator 1111 --KVS /tmp/chip-ota-requestor-kvs1 --otaDownloadPath /tmp/chip-ota-requestor-downloaded-image1 --autoApplyImage > >(tee /tmp/darwin/framework-tests/ota-requestor-app-1.log) 2> >(tee /tmp/darwin/framework-tests/ota-requestor-app-err-1.log >&2) & + ../../../out/debug/chip-ota-requestor-app --interface-id -1 --secured-device-port 5543 --discriminator 1112 --KVS /tmp/chip-ota-requestor-kvs2 --otaDownloadPath /tmp/chip-ota-requestor-downloaded-image2 --autoApplyImage > >(tee /tmp/darwin/framework-tests/ota-requestor-app-2.log) 2> >(tee /tmp/darwin/framework-tests/ota-requestor-app-err-2.log >&2) & xcodebuild test -target "Matter" -scheme "Matter Framework Tests" -sdk macosx OTHER_CFLAGS='${inherited} -Werror -Wconversion -Wno-incomplete-umbrella -Wno-unguarded-availability-new' > >(tee /tmp/darwin/framework-tests/darwin-tests.log) 2> >(tee /tmp/darwin/framework-tests/darwin-tests-err.log >&2) working-directory: src/darwin/Framework - name: Uploading log files diff --git a/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m b/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m index 901bf7ef06862e..8b5b0f7c9dcf97 100644 --- a/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m +++ b/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m @@ -28,6 +28,7 @@ static const uint16_t kPairingTimeoutInSeconds = 10; static const uint16_t kTimeoutInSeconds = 3; +static const uint16_t kTimeoutWithUpdateInSeconds = 10; static const uint64_t kDeviceId1 = 0x12341234; static const uint64_t kDeviceId2 = 0x12341235; // NOTE: These onboarding payloads are for the chip-ota-requestor-app, not chip-all-clusters-app @@ -123,6 +124,7 @@ - (void)handleQueryImageForNodeID:(NSNumber *)nodeID if (self.queryImageHandler) { self.queryImageHandler(nodeID, controller, params, completion); } else { + XCTFail(@"Unexpected attempt to query for an image"); [self respondNotAvailableWithCompletion:completion]; } } @@ -137,6 +139,7 @@ - (void)handleApplyUpdateRequestForNodeID:(NSNumber *)nodeID if (self.applyUpdateRequestHandler) { self.applyUpdateRequestHandler(nodeID, controller, params, completion); } else { + XCTFail(@"Unexpected attempt to apply an update"); [self respondWithErrorToApplyUpdateRequestWithCompletion:completion]; } } @@ -151,6 +154,7 @@ - (void)handleNotifyUpdateAppliedForNodeID:(NSNumber *)nodeID if (self.notifyUpdateAppliedHandler) { self.notifyUpdateAppliedHandler(nodeID, controller, params, completion); } else { + XCTFail(@"Unexpected update application"); [self respondErrorWithCompletion:completion]; } } @@ -166,6 +170,7 @@ - (void)handleBDXTransferSessionBeginForNodeID:(NSNumber *)nodeID if (self.transferBeginHandler) { self.transferBeginHandler(nodeID, controller, fileDesignator, offset, completion); } else { + XCTFail(@"Unexpected attempt to begin BDX transfer"); [self respondErrorWithCompletion:completion]; } } @@ -182,6 +187,7 @@ - (void)handleBDXQueryForNodeID:(NSNumber *)nodeID if (self.blockQueryHandler) { self.blockQueryHandler(nodeID, controller, blockSize, blockIndex, bytesToSkip, completion); } else { + XCTFail(@"Unexpected attempt to get BDX block"); completion(nil, YES); } } @@ -192,6 +198,8 @@ - (void)handleBDXTransferSessionEndForNodeID:(NSNumber *)nodeID { if (self.transferEndHandler) { self.transferEndHandler(nodeID, controller, error); + } else { + XCTFail(@"Unexpected end of BDX transfer"); } } @@ -210,7 +218,10 @@ - (void)respondBusyWithDelay:(NSNumber *)delay completion:(QueryImageCompletion) completion(responseParams, nil); } -- (void)respondAvailableWithDelay:(NSNumber *)delay uri:(NSString *)uri completion:(QueryImageCompletion)completion +- (void)respondAvailableWithDelay:(NSNumber *)delay + uri:(NSString *)uri + updateToken:(NSData *)updateToken + completion:(QueryImageCompletion)completion { __auto_type * responseParams = [[MTROTASoftwareUpdateProviderClusterQueryImageResponseParams alloc] init]; responseParams.status = @(MTROTASoftwareUpdateProviderOTAQueryStatusUpdateAvailable); @@ -220,8 +231,7 @@ - (void)respondAvailableWithDelay:(NSNumber *)delay uri:(NSString *)uri completi // SoftwareVersion/SoftwareVersionString/UpdateToken bits. responseParams.softwareVersion = @(18); responseParams.softwareVersionString = @"18"; - const char updateToken[] = "12345678"; - responseParams.updateToken = [NSData dataWithBytes:updateToken length:sizeof(updateToken)]; + responseParams.updateToken = updateToken; completion(responseParams, nil); } @@ -232,6 +242,14 @@ - (void)respondWithErrorToApplyUpdateRequestWithCompletion:(ApplyUpdateRequestCo }]; } +- (void)respondWithDiscontinueToApplyUpdateRequestWithCompletion:(ApplyUpdateRequestCompletion)completion +{ + __auto_type * params = [[MTROTASoftwareUpdateProviderClusterApplyUpdateResponseParams alloc] init]; + params.action = @(MTROTASoftwareUpdateProviderOTAApplyUpdateActionDiscontinue); + params.delayedActionTime = @(0); + completion(params, nil); +} + - (void)respondErrorWithCompletion:(MTRStatusCompletion)completion { [self respondErrorWithCode:MTRErrorCodeGeneralError completion:completion]; @@ -252,6 +270,19 @@ - (void)respondSuccess:(MTRStatusCompletion)completion completion(nil); } +- (NSData *)generateUpdateToken +{ + const size_t dataSize = 16; + const size_t randomBytesAtOnce = sizeof(uint32_t); + XCTAssertEqual(dataSize % randomBytesAtOnce, 0); + NSMutableData * data = [NSMutableData dataWithCapacity:16]; + for (unsigned i = 0; i < dataSize / randomBytesAtOnce; ++i) { + uint32_t randomBytes = arc4random(); + [data appendBytes:&randomBytes length:randomBytesAtOnce]; + } + return [NSData dataWithData:data]; +} + @end static MTROTAProviderDelegateImpl * sOTAProviderDelegate; @@ -502,7 +533,10 @@ - (void)test003_ReceiveSecondQueryWhileHandlingBDX MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) { XCTAssertEqualObjects(nodeID, @(kDeviceId1)); XCTAssertEqual(controller, sController); - [sOTAProviderDelegate respondAvailableWithDelay:@(0) uri:fakeImageURI completion:completion]; + [sOTAProviderDelegate respondAvailableWithDelay:@(0) + uri:fakeImageURI + updateToken:[sOTAProviderDelegate generateUpdateToken] + completion:completion]; [queryExpectation1 fulfill]; }; sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSString * fileDesignator, @@ -516,11 +550,7 @@ - (void)test003_ReceiveSecondQueryWhileHandlingBDX sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion innerCompletion) { sOTAProviderDelegate.queryImageHandler = handleThirdQuery; - sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, - NSString * fileDesignator, NSNumber * offset, MTRStatusCompletion completion) { - // Should be no more queries. - XCTFail(@"Unexpected attempt to begin BDX transfer"); - }; + sOTAProviderDelegate.transferBeginHandler = nil; XCTAssertEqualObjects(nodeID, @(kDeviceId2)); XCTAssertEqual(controller, sController); @@ -528,7 +558,10 @@ - (void)test003_ReceiveSecondQueryWhileHandlingBDX // We respond UpdateAvailable, but since we are in the middle of // handling OTA for device1 we expect the requestor to get Busy and // try again. - [sOTAProviderDelegate respondAvailableWithDelay:@(busyDelay) uri:fakeImageURI completion:innerCompletion]; + [sOTAProviderDelegate respondAvailableWithDelay:@(busyDelay) + uri:fakeImageURI + updateToken:[sOTAProviderDelegate generateUpdateToken] + completion:innerCompletion]; [sOTAProviderDelegate respondErrorWithCompletion:outerCompletion]; [queryExpectation2 fulfill]; }; @@ -557,6 +590,177 @@ - (void)test003_ReceiveSecondQueryWhileHandlingBDX [self waitForExpectations:@[ announceResponseExpectation1, announceResponseExpectation2 ] timeout:kTimeoutInSeconds]; } +- (void)test004_DoBDXTransferDenyUpdateRequest +{ + // In this test we do the following: + // + // 1) Create an actual image we can send to the device, with a valid header + // but garbage data. + // 2) Advertise ourselves to device. + // 3) When device queries for an image, claim to have one. + // 4) When device tries to start a bdx transfer, respond with success. + // 5) Send the data as the BDX transfer proceeds. + // 6) When device invokes ApplyUpdateRequest, respond with Discontinue so + // that the update does not actually proceed. + __auto_type * device = sConnectedDevice1; + + XCTestExpectation * queryExpectation = [self expectationWithDescription:@"handleQueryImageForNodeID called"]; + XCTestExpectation * bdxBeginExpectation = [self expectationWithDescription:@"handleBDXTransferSessionBeginForNodeID called"]; + XCTestExpectation * bdxQueryExpectation = [self expectationWithDescription:@"handleBDXQueryForNodeID called"]; + XCTestExpectation * bdxEndExpectation = [self expectationWithDescription:@"handleBDXTransferSessionEndForNodeID called"]; + XCTestExpectation * applyUpdateRequestExpectation = + [self expectationWithDescription:@"handleApplyUpdateRequestForNodeID called"]; + + NSData * updateToken = [sOTAProviderDelegate generateUpdateToken]; + + // First, create an image. Make it at least 4096 bytes long, so we get + // multiple BDX blocks going. + const size_t rawImageSize = 4112; + NSData * rawImagePiece = [@"1234567890abcdef" dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertEqual(rawImageSize % rawImagePiece.length, 0); + NSMutableData * fakeImage = [NSMutableData dataWithCapacity:rawImageSize]; + while (fakeImage.length < rawImageSize) { + [fakeImage appendData:rawImagePiece]; + } + NSString * rawImagePath = @"/tmp/test004-raw-image"; + NSString * imagePath = @"/tmp/test004-image"; + + [[NSFileManager defaultManager] createFileAtPath:rawImagePath contents:fakeImage attributes:nil]; + + // Find the right absolute path to our ota_image_tool.py script. PWD should + // point to our src/darwin/Framework, while the script is in + // src/app/ota_image_tool.py. + NSString * pwd = [[NSProcessInfo processInfo] environment][@"PWD"]; + NSString * imageToolPath = [NSString + pathWithComponents:@[ [pwd substringToIndex:(pwd.length - @"darwin/Framework".length)], @"app", @"ota_image_tool.py" ]]; + + NSTask * task = [[NSTask alloc] init]; + [task setLaunchPath:imageToolPath]; + [task setArguments:@[ + @"create", @"-v", @"0xFFF1", @"-p", @"0x8001", @"-vn", @"2", @"-vs", @"2.0", @"-da", @"sha256", rawImagePath, imagePath + ]]; + NSError * launchError = nil; + [task launchAndReturnError:&launchError]; + XCTAssertNil(launchError); + [task waitUntilExit]; + XCTAssertEqual([task terminationStatus], 0); + + __block NSFileHandle * readHandle; + __block uint64_t imageSize; + __block uint32_t lastBlockIndex = UINT32_MAX; + + // TODO: Maybe we should move more of this logic into sOTAProviderDelegate + // or some other helper, once we have multiple tests sending images? For + // example, we could have something where you can do one of two things: + // + // 1) register a "raw image" with it, and it generates the + // image-with header. + // 2) register a pre-generated image with it and it uses "ota_image_tool.py + // extract" to extract the raw image. + // + // Once that's done the helper could track the transfer state for a + // particular image, etc, with us just forwarding our notifications to it. + sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, + MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId1)); + XCTAssertEqual(controller, sController); + + sOTAProviderDelegate.queryImageHandler = nil; + [sOTAProviderDelegate respondAvailableWithDelay:@(0) uri:imagePath updateToken:updateToken completion:completion]; + [queryExpectation fulfill]; + }; + sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSString * fileDesignator, + NSNumber * offset, MTRStatusCompletion completion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId1)); + XCTAssertEqual(controller, sController); + XCTAssertEqualObjects(fileDesignator, imagePath); + XCTAssertEqualObjects(offset, @(0)); + + readHandle = [NSFileHandle fileHandleForReadingAtPath:fileDesignator]; + XCTAssertNotNil(readHandle); + + NSError * endSeekError; + XCTAssertTrue([readHandle seekToEndReturningOffset:&imageSize error:&endSeekError]); + XCTAssertNil(endSeekError); + + sOTAProviderDelegate.transferBeginHandler = nil; + [sOTAProviderDelegate respondSuccess:completion]; + [bdxBeginExpectation fulfill]; + }; + sOTAProviderDelegate.blockQueryHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSNumber * blockSize, + NSNumber * blockIndex, NSNumber * bytesToSkip, BlockQueryCompletion completion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId1)); + XCTAssertEqual(controller, sController); + XCTAssertEqualObjects(blockSize, @(1024)); // Seems to always be 1024. + XCTAssertEqualObjects(blockIndex, @(lastBlockIndex + 1)); + XCTAssertEqualObjects(bytesToSkip, @(0)); // Don't expect to see skips here. + // Make sure we actually end up with multiple blocks. + XCTAssertTrue(blockSize.unsignedLongLongValue < rawImageSize); + + XCTAssertNotNil(readHandle); + uint64_t offset = blockSize.unsignedLongLongValue * blockIndex.unsignedLongLongValue; + NSError * seekError = nil; + [readHandle seekToOffset:offset error:&seekError]; + XCTAssertNil(seekError); + + NSError * readError = nil; + NSData * data = [readHandle readDataUpToLength:blockSize.unsignedLongValue error:&readError]; + XCTAssertNil(readError); + XCTAssertNotNil(data); + + BOOL isEOF = offset + blockSize.unsignedLongValue >= imageSize; + + ++lastBlockIndex; + + if (isEOF) { + sOTAProviderDelegate.blockQueryHandler = nil; + } + + completion(data, isEOF); + + if (isEOF) { + [bdxQueryExpectation fulfill]; + } + }; + sOTAProviderDelegate.transferEndHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSError * _Nullable error) { + XCTAssertEqualObjects(nodeID, @(kDeviceId1)); + XCTAssertEqual(controller, sController); + XCTAssertNil(error); + + sOTAProviderDelegate.transferEndHandler = nil; + [bdxEndExpectation fulfill]; + }; + sOTAProviderDelegate.applyUpdateRequestHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, + MTROTASoftwareUpdateProviderClusterApplyUpdateRequestParams * params, ApplyUpdateRequestCompletion completion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId1)); + XCTAssertEqual(controller, sController); + XCTAssertEqualObjects(params.updateToken, updateToken); + XCTAssertEqualObjects(params.newVersion, @(18)); // TODO: Factor this out better! + + XCTAssertTrue([[NSFileManager defaultManager] contentsEqualAtPath:rawImagePath + andPath:@"/tmp/chip-ota-requestor-downloaded-image1"]); + + sOTAProviderDelegate.applyUpdateRequestHandler = nil; + [sOTAProviderDelegate respondWithDiscontinueToApplyUpdateRequestWithCompletion:completion]; + [applyUpdateRequestExpectation fulfill]; + }; + + // Advertise ourselves as an OTA provider. + XCTestExpectation * announceResponseExpectation = [self announceProviderToDevice:device]; + + // Make sure we get our callbacks in order. Give it a bit more time, because + // we want to allow time for the BDX download. + [self waitForExpectations:@[ queryExpectation, bdxBeginExpectation, bdxQueryExpectation ] + timeout:(kTimeoutWithUpdateInSeconds) enforceOrder:YES]; + + // Nothing really defines the ordering of bdxEndExpectation and + // applyUpdateRequestExpectation with respect to each other, and nothing + // defines the ordering of announceResponseExpectation with respect to _any_ + // of the above expectations. + [self waitForExpectations:@[ bdxEndExpectation, applyUpdateRequestExpectation, announceResponseExpectation ] + timeout:kTimeoutInSeconds]; +} + - (void)test999_TearDown { __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(kDeviceId1) controller:sController];