From e8489cab0436b7e25a04fff13d96ac01fd1d1750 Mon Sep 17 00:00:00 2001 From: Nivi Sarkar <55898241+nivi-apple@users.noreply.github.com> Date: Fri, 28 Jul 2023 17:09:04 -0700 Subject: [PATCH] Add more test cases to the OTA provider test (#27874) * Add more test cases to the OTA provider test - Add a test to update and verify the default OTA provider - Add a test for BDX transfer between the provider and two OTA requestors - Add a test for incremental OTA update * Restyled by whitespace * Restyled by clang-format * Apply suggestions from code review Co-authored-by: Boris Zbarsky * Review comments * Add check for discriminator 1113 to MTRCommissionableBrowserTests.m * Restyled by clang-format * Fix kExpectedDiscoveredDevicesCount for MTRCommissionableBrowserTests and onboarding payload for ota requestor 3 for MTROTAProviderTests * Decommission device3 after OTA tests are done * Update src/darwin/Framework/CHIPTests/MTROTAProviderTests.m Co-authored-by: Boris Zbarsky * Apply suggestions from code review Co-authored-by: Boris Zbarsky * Addressed review comments * Apply suggestions from code review Co-authored-by: Boris Zbarsky * more review comments --------- Co-authored-by: Restyled.io Co-authored-by: Boris Zbarsky --- .github/workflows/darwin.yaml | 7 +- .../CHIPTests/MTRCommissionableBrowserTests.m | 10 +- .../Framework/CHIPTests/MTROTAProviderTests.m | 575 +++++++++++++++++- 3 files changed, 563 insertions(+), 29 deletions(-) diff --git a/.github/workflows/darwin.yaml b/.github/workflows/darwin.yaml index 1db7aa8a082846..c30cc044f35b33 100644 --- a/.github/workflows/darwin.yaml +++ b/.github/workflows/darwin.yaml @@ -95,10 +95,12 @@ jobs: - name: Build example OTA Provider run: | scripts/examples/gn_build_example.sh examples/ota-provider-app/linux out/debug chip_config_network_layer_ble=false - - name: Build OTA image files with software version number 5 + - name: Build OTA image files with software version number 5 and 10 run: | scripts/examples/gn_build_example.sh examples/ota-requestor-app/linux out/debug chip_config_network_layer_ble=false non_spec_compliant_ota_action_delay_floor=0 chip_device_config_device_software_version=5 chip_device_config_device_software_version_string='"5.0"' - cp out/debug/chip-ota-requestor-app /tmp/ota-test005-raw-image + cp out/debug/chip-ota-requestor-app /tmp/ota-raw-image-v5 + scripts/examples/gn_build_example.sh examples/ota-requestor-app/linux out/debug chip_config_network_layer_ble=false non_spec_compliant_ota_action_delay_floor=0 chip_device_config_device_software_version=10 chip_device_config_device_software_version_string='"10.0"' + cp out/debug/chip-ota-requestor-app /tmp/ota-raw-image-v10 - name: Build example OTA Requestor run: | scripts/examples/gn_build_example.sh examples/ota-requestor-app/linux out/debug chip_config_network_layer_ble=false non_spec_compliant_ota_action_delay_floor=0 @@ -120,6 +122,7 @@ jobs: # 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 --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) & + ../../../out/debug/chip-ota-requestor-app --interface-id -1 --secured-device-port 5544 --discriminator 1113 --KVS /tmp/chip-ota-requestor-kvs3 --otaDownloadPath /tmp/chip-ota-requestor-downloaded-image3 --autoApplyImage > >(tee /tmp/darwin/framework-tests/ota-requestor-app-3.log) 2> >(tee /tmp/darwin/framework-tests/ota-requestor-app-err-3.log >&2) & # Disable BLE because the app does not have the permission to use # it and that may crash the CI. # diff --git a/src/darwin/Framework/CHIPTests/MTRCommissionableBrowserTests.m b/src/darwin/Framework/CHIPTests/MTRCommissionableBrowserTests.m index b2648b2956b3ee..c984ac670a8500 100644 --- a/src/darwin/Framework/CHIPTests/MTRCommissionableBrowserTests.m +++ b/src/darwin/Framework/CHIPTests/MTRCommissionableBrowserTests.m @@ -29,10 +29,11 @@ static const uint16_t kTestProductId2 = 0x8001u; static const uint16_t kTestDiscriminator1 = 1111u; static const uint16_t kTestDiscriminator2 = 1112u; -static const uint16_t kTestDiscriminator3 = 3840u; -static const uint16_t kTestDiscriminator4 = 3839u; +static const uint16_t kTestDiscriminator3 = 1113u; +static const uint16_t kTestDiscriminator4 = 3840u; +static const uint16_t kTestDiscriminator5 = 3839u; static const uint16_t kDiscoverDeviceTimeoutInSeconds = 10; -static const uint16_t kExpectedDiscoveredDevicesCount = 4; +static const uint16_t kExpectedDiscoveredDevicesCount = 5; // Singleton controller we use. static MTRDeviceController * sController = nil; @@ -77,7 +78,8 @@ - (void)controller:(MTRDeviceController *)controller didFindCommissionableDevice XCTAssertEqualObjects(vendorId, @(kTestVendorId)); XCTAssertTrue([productId isEqual:@(kTestProductId1)] || [productId isEqual:@(kTestProductId2)]); XCTAssertTrue([discriminator isEqual:@(kTestDiscriminator1)] || [discriminator isEqual:@(kTestDiscriminator2)] || - [discriminator isEqual:@(kTestDiscriminator3)] || [discriminator isEqual:@(kTestDiscriminator4)]); + [discriminator isEqual:@(kTestDiscriminator3)] || [discriminator isEqual:@(kTestDiscriminator4)] || + [discriminator isEqual:@(kTestDiscriminator5)]); XCTAssertEqual(commissioningMode, YES); NSLog(@"Found Device (%@) with discriminator: %@ (vendor: %@, product: %@)", instanceName, discriminator, vendorId, productId); diff --git a/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m b/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m index 22f7ff6d5571fe..472bd22d9919c7 100644 --- a/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m +++ b/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m @@ -32,11 +32,13 @@ #define ENABLE_OTA_TESTS 1 #endif -// TODO: Disable test005_DoBDXTransferAllowUpdateRequest until PR #26040 is merged. +// TODO: Disable test005_DoBDXTransferAllowUpdateRequest, +// test006_DoBDXTransferWithTwoOTARequesters and +// test007_DoBDXTransferIncrementalOtaUpdate until PR #26040 is merged. // Currently the poll interval causes delays in the BDX transfer and // results in the test taking a long time. -#ifdef ENABLE_TEST_005 -#undef ENABLE_TEST_005 +#ifdef ENABLE_REAL_OTA_UPDATE_TESTS +#undef ENABLE_REAL_OTA_UPDATE_TESTS #endif static const uint16_t kPairingTimeoutInSeconds = 10; @@ -44,9 +46,11 @@ static const uint16_t kTimeoutWithUpdateInSeconds = 60; static const uint64_t kDeviceId1 = 0x12341234; static const uint64_t kDeviceId2 = 0x12341235; +static const uint64_t kDeviceId3 = 0x12341236; // NOTE: These onboarding payloads are for the chip-ota-requestor-app, not chip-all-clusters-app static NSString * kOnboardingPayload1 = @"MT:-24J0SO527K10648G00"; // Discriminator: 1111 static NSString * kOnboardingPayload2 = @"MT:-24J0AFN00L10648G00"; // Discriminator: 1112 +static NSString * kOnboardingPayload3 = @"MT:-24J0IRV01L10648G00"; // Discriminator: 1113 static const uint16_t kLocalPort = 5541; static const uint16_t kTestVendorId = 0xFFF1u; @@ -54,6 +58,7 @@ static MTRDevice * sConnectedDevice1; static MTRDevice * sConnectedDevice2; +static MTRDevice * sConnectedDevice3; // Singleton controller we use. static MTRDeviceController * sController = nil; @@ -63,9 +68,17 @@ static NSString * kOtaDownloadedFilePath1 = @"/tmp/chip-ota-requestor-downloaded-image1"; -static NSNumber * kUpdatedSoftwareVersion = @5; +static NSString * kOtaDownloadedFilePath2 = @"/tmp/chip-ota-requestor-downloaded-image2"; -static NSString * kUpdatedSoftwareVersionString = @"5.0"; +static NSString * kOtaDownloadedFilePath3 = @"/tmp/chip-ota-requestor-downloaded-image3"; + +static NSNumber * kUpdatedSoftwareVersion_5 = @5; + +static NSString * kUpdatedSoftwareVersionString_5 = @"5.0"; + +static NSNumber * kUpdatedSoftwareVersion_10 = @10; + +static NSString * kUpdatedSoftwareVersionString_10 = @"10.0"; @interface MTROTAProviderTestControllerDelegate : NSObject @property (nonatomic, readonly) XCTestExpectation * expectation; @@ -240,6 +253,8 @@ - (void)respondBusyWithDelay:(NSNumber *)delay completion:(QueryImageCompletion) - (void)respondAvailableWithDelay:(NSNumber *)delay uri:(NSString *)uri updateToken:(NSData *)updateToken + softwareVersion:(NSNumber *)softwareVersion + softwareVersionString:(NSString *)softwareVersionString completion:(QueryImageCompletion)completion { __auto_type * responseParams = [[MTROTASoftwareUpdateProviderClusterQueryImageResponseParams alloc] init]; @@ -248,8 +263,8 @@ - (void)respondAvailableWithDelay:(NSNumber *)delay responseParams.imageURI = uri; // TODO: Figure out whether we need better // SoftwareVersion/SoftwareVersionString/UpdateToken bits. - responseParams.softwareVersion = kUpdatedSoftwareVersion; - responseParams.softwareVersionString = kUpdatedSoftwareVersionString; + responseParams.softwareVersion = softwareVersion; + responseParams.softwareVersionString = softwareVersionString; responseParams.updateToken = updateToken; completion(responseParams, nil); } @@ -314,7 +329,10 @@ - (NSData *)generateUpdateToken @interface MTROTAProviderTransferChecker : NSObject - (instancetype)initWithRawImagePath:(NSString *)rawImagePath + otaImageDownloadFilePath:(NSString *)otaImageDownloadFilePath nodeID:(NSNumber *)nodeID + softwareVersion:(NSNumber *)softwareVersion + softwareVersionString:(NSString *)softwareVersionString applyUpdateAction:(MTROTASoftwareUpdateProviderOTAApplyUpdateAction)applyUpdateAction testcase:(XCTestCase *)testcase; @@ -329,7 +347,10 @@ - (instancetype)initWithRawImagePath:(NSString *)rawImagePath @implementation MTROTAProviderTransferChecker - (instancetype)initWithRawImagePath:(NSString *)rawImagePath + otaImageDownloadFilePath:(NSString *)otaImageDownloadFilePath nodeID:(NSNumber *)nodeID + softwareVersion:(NSNumber *)softwareVersion + softwareVersionString:(NSString *)softwareVersionString applyUpdateAction:(MTROTASoftwareUpdateProviderOTAApplyUpdateAction)applyUpdateAction testcase:(XCTestCase *)testcase { @@ -357,8 +378,8 @@ - (instancetype)initWithRawImagePath:(NSString *)rawImagePath NSTask * task = [[NSTask alloc] init]; [task setLaunchPath:imageToolPath]; [task setArguments:@[ - @"create", @"-v", @"0xFFF1", @"-p", @"0x8001", @"-vn", [kUpdatedSoftwareVersion stringValue], @"-vs", - kUpdatedSoftwareVersionString, @"-da", @"sha256", rawImagePath, imagePath + @"create", @"-v", @"0xFFF1", @"-p", @"0x8001", @"-vn", [softwareVersion stringValue], @"-vs", softwareVersionString, @"-da", + @"sha256", rawImagePath, imagePath ]]; NSError * launchError = nil; [task launchAndReturnError:&launchError]; @@ -379,7 +400,12 @@ - (instancetype)initWithRawImagePath:(NSString *)rawImagePath XCTAssertEqual(controller, sController); sOTAProviderDelegate.queryImageHandler = nil; - [sOTAProviderDelegate respondAvailableWithDelay:@(0) uri:imagePath updateToken:updateToken completion:completion]; + [sOTAProviderDelegate respondAvailableWithDelay:@(0) + uri:imagePath + updateToken:updateToken + softwareVersion:softwareVersion + softwareVersionString:softwareVersionString + completion:completion]; [self.queryExpectation fulfill]; }; sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSString * fileDesignator, @@ -448,9 +474,9 @@ - (instancetype)initWithRawImagePath:(NSString *)rawImagePath XCTAssertEqualObjects(nodeID, nodeID); XCTAssertEqual(controller, sController); XCTAssertEqualObjects(params.updateToken, updateToken); - XCTAssertEqualObjects(params.newVersion, kUpdatedSoftwareVersion); // TODO: Factor this out better! + XCTAssertEqualObjects(params.newVersion, softwareVersion); - XCTAssertTrue([[NSFileManager defaultManager] contentsEqualAtPath:rawImagePath andPath:kOtaDownloadedFilePath1]); + XCTAssertTrue([[NSFileManager defaultManager] contentsEqualAtPath:rawImagePath andPath:otaImageDownloadFilePath]); sOTAProviderDelegate.applyUpdateRequestHandler = nil; [sOTAProviderDelegate respondToApplyUpdateRequestWithAction:applyUpdateAction completion:completion]; @@ -463,7 +489,7 @@ - (instancetype)initWithRawImagePath:(NSString *)rawImagePath XCTAssertEqualObjects(nodeID, nodeID); XCTAssertEqual(controller, sController); XCTAssertEqualObjects(params.updateToken, updateToken); - XCTAssertEqualObjects(params.softwareVersion, kUpdatedSoftwareVersion); + XCTAssertEqualObjects(params.softwareVersion, softwareVersion); sOTAProviderDelegate.notifyUpdateAppliedHandler = nil; [sOTAProviderDelegate respondSuccess:completion]; @@ -585,6 +611,7 @@ - (void)initStack sConnectedDevice1 = [self commissionDeviceWithPayload:kOnboardingPayload1 nodeID:@(kDeviceId1)]; sConnectedDevice2 = [self commissionDeviceWithPayload:kOnboardingPayload2 nodeID:@(kDeviceId2)]; + sConnectedDevice3 = [self commissionDeviceWithPayload:kOnboardingPayload3 nodeID:@(kDeviceId3)]; } + (void)shutdownStack @@ -634,7 +661,7 @@ - (XCTestExpectation *)announceProviderToDevice:(MTRDevice *)device return responseExpectation; } -- (void)test001_ReceiveOTAQuery +- (void)test001_ReceiveQueryImageRequest_RespondUpdateNotAvailable { // Test that if we advertise ourselves as a provider we end up getting a // QueryImage callbacks that we can respond to. @@ -657,7 +684,7 @@ - (void)test001_ReceiveOTAQuery sOTAProviderDelegate.queryImageHandler = nil; } -- (void)test002_ReceiveTwoQueriesExplicitBusy +- (void)test002_ReceiveTwoQueryImageRequests_RespondExplicitBusy { // Test that if we advertise ourselves as a provider and respond BUSY to // QueryImage callback, then we get a second QueryImage callback later on @@ -696,7 +723,7 @@ - (void)test002_ReceiveTwoQueriesExplicitBusy [self waitForExpectations:@[ announceResponseExpectation ] timeout:kTimeoutInSeconds]; } -- (void)test003_ReceiveSecondQueryWhileHandlingBDX +- (void)test003_ReceiveQueryImageRequestWhileHandlingBDX_RespondImplicitBusy { // In this test we do the following: // @@ -726,6 +753,8 @@ - (void)test003_ReceiveSecondQueryWhileHandlingBDX [sOTAProviderDelegate respondAvailableWithDelay:@(0) uri:fakeImageURI updateToken:[sOTAProviderDelegate generateUpdateToken] + softwareVersion:kUpdatedSoftwareVersion_5 + softwareVersionString:kUpdatedSoftwareVersionString_5 completion:completion]; [queryExpectation1 fulfill]; }; @@ -751,6 +780,8 @@ - (void)test003_ReceiveSecondQueryWhileHandlingBDX [sOTAProviderDelegate respondAvailableWithDelay:@(busyDelay) uri:fakeImageURI updateToken:[sOTAProviderDelegate generateUpdateToken] + softwareVersion:kUpdatedSoftwareVersion_5 + softwareVersionString:kUpdatedSoftwareVersionString_5 completion:innerCompletion]; [sOTAProviderDelegate respondErrorWithCompletion:outerCompletion]; [queryExpectation2 fulfill]; @@ -809,7 +840,10 @@ - (void)test004_DoBDXTransferDenyUpdateRequest __auto_type * checker = [[MTROTAProviderTransferChecker alloc] initWithRawImagePath:rawImagePath + otaImageDownloadFilePath:kOtaDownloadedFilePath1 nodeID:@(kDeviceId1) + softwareVersion:kUpdatedSoftwareVersion_5 + softwareVersionString:kUpdatedSoftwareVersionString_5 applyUpdateAction:MTROTASoftwareUpdateProviderOTAApplyUpdateActionDiscontinue testcase:self]; // We do not expect the update to actually be applied here. @@ -837,10 +871,10 @@ - (void)test004_DoBDXTransferDenyUpdateRequest [self waitForExpectations:@[ checker.notifyUpdateAppliedExpectation ] timeout:kTimeoutInSeconds]; } -// TODO: Enable this test when PR #26040 is merged. Currently the poll interval causes delays in the BDX transfer and -// results in the test taking a long time. With PR #26040 we eliminate the poll interval completely and hence the test -// can run in a short time. -#ifdef ENABLE_TEST_005 +// TODO: Enable tests 005, 006 and 007 when PR #26040 is merged. Currently the poll interval causes delays in the BDX transfer +// and results in the tests taking a long time. With PR #26040 we eliminate the poll interval completely and hence the tests can run +// in a short time. +#ifdef ENABLE_REAL_OTA_UPDATE_TESTS - (void)test005_DoBDXTransferAllowUpdateRequest { // In this test we do the following: @@ -855,16 +889,476 @@ - (void)test005_DoBDXTransferAllowUpdateRequest // 8) Wait for the app to restart and wait for the NotifyUpdateApplied message to confirm the app has updated to the new version // This test expects a pre-generated raw image at otaRawImagePath. - NSString * otaRawImagePath = @"/tmp/ota-test005-raw-image"; + NSString * otaRawImagePath = @"/tmp/ota-raw-image-v5"; - // Check if the ota raw image exists at kOtaRawImagePath + // Check whether the ota raw image exists at otaRawImagePath XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:otaRawImagePath]); __auto_type * device = sConnectedDevice1; __auto_type * checker = [[MTROTAProviderTransferChecker alloc] initWithRawImagePath:otaRawImagePath + otaImageDownloadFilePath:kOtaDownloadedFilePath1 nodeID:@(kDeviceId1) + softwareVersion:kUpdatedSoftwareVersion_5 + softwareVersionString:kUpdatedSoftwareVersionString_5 + applyUpdateAction:MTROTASoftwareUpdateProviderOTAApplyUpdateActionProceed + testcase:self]; + + // 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:@[ + checker.queryExpectation, checker.bdxBeginExpectation, checker.bdxQueryExpectation, checker.bdxEndExpectation + ] + timeout:(kTimeoutWithUpdateInSeconds) enforceOrder:YES]; + + // Nothing really defines the ordering of bdxEndExpectation and + // applyUpdateRequestExpectation with respect to each other. + [self waitForExpectations:@[ checker.applyUpdateRequestExpectation, checker.notifyUpdateAppliedExpectation ] + timeout:kTimeoutInSeconds + enforceOrder:YES]; + + // Nothing defines the ordering of announceResponseExpectation with respect + // to _any_ of the above expectations. + [self waitForExpectations:@[ announceResponseExpectation ] timeout:kTimeoutInSeconds]; +} + +- (void)test006_DoBDXTransferWithTwoOTARequesters +{ + // Note: This test has a dependency on test005_DoBDXTransferAllowUpdateRequest since we update device1 to version + // number 5 in the above test. We reuse device1 for this test and we need to use an OTA image with a higher version number (10) + // for device1 to update itself again. We need to fix this when we want to run tests out of order. + + // In this test, we test BDX transfers between one provider and two OTA requestors device1 and device2. + // + // 1) We announce ourselves to device1 first. + // 2) When device1 queries, we return image available at imagePath1. + // 3) We set the BDX begin and end handlers for device1 and device1 starts the BDX transfer. + // 4) At this time, we announce ourselves to device2. + // 5) When device2 queries, we return image available with a busy delay of 30 secs. + // 6) When device1 completes the BDX transfer, we wait for device2 to query. + // 7) When device 2 queries again, we return image available at imagePath2. + // 8) We set the BDX begin and end handlers for device2 and device2 starts the BDX transfer. + // 9) Device1 applies the update independently and reboots with the new image. + // 10) At this point, we set the apply update handlers for device2. + // 11) Device2 applies the update and reboots with its new image. + + // This test expects a pre-generated raw image at otaRawImagePath1 for device1 and at otaRawImagePath2 for device2. + NSString * otaRawImagePath1 = @"/tmp/ota-raw-image-v10"; + NSString * otaRawImagePath2 = @"/tmp/ota-raw-image-v5"; + + // Check whether the ota raw image exists at otaRawImagePath1 and otaRawImagePath2 + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:otaRawImagePath1]); + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:otaRawImagePath2]); + + XCTestExpectation * queryExpectation1 = [self expectationWithDescription:@"handleQueryImageForNodeID called for device1"]; + XCTestExpectation * queryExpectation2 = + [self expectationWithDescription:@"handleQueryImageForNodeID called for device2 - busy"]; + XCTestExpectation * queryExpectation3 = + [self expectationWithDescription:@"handleQueryImageForNodeID called for device2 - available"]; + + XCTestExpectation * bdxBeginExpectation1 = + [self expectationWithDescription:@"handleBDXTransferSessionBeginForNodeID called for device1"]; + XCTestExpectation * bdxBeginExpectation2 = + [self expectationWithDescription:@"handleBDXTransferSessionBeginForNodeID called for device2"]; + + XCTestExpectation * bdxQueryExpectation1 = [self expectationWithDescription:@"handleBDXQueryForNodeID called for device1"]; + XCTestExpectation * bdxQueryExpectation2 = [self expectationWithDescription:@"handleBDXQueryForNodeID called for device2"]; + + XCTestExpectation * bdxEndExpectation1 = + [self expectationWithDescription:@"handleBDXTransferSessionEndForNodeID called for device1"]; + XCTestExpectation * bdxEndExpectation2 = + [self expectationWithDescription:@"handleBDXTransferSessionEndForNodeID called for device2"]; + + XCTestExpectation * applyUpdateRequestExpectation1 = + [self expectationWithDescription:@"handleApplyUpdateRequestForNodeID called for device1"]; + XCTestExpectation * applyUpdateRequestExpectation2 = + [self expectationWithDescription:@"handleApplyUpdateRequestForNodeID called for device2"]; + + XCTestExpectation * notifyUpdateAppliedExpectation1 = + [self expectationWithDescription:@"handleNotifyUpdateAppliedForNodeID called for device1"]; + XCTestExpectation * notifyUpdateAppliedExpectation2 = + [self expectationWithDescription:@"handleNotifyUpdateAppliedForNodeID called for device2"]; + + __block XCTestExpectation * announceResponseExpectation2; + + NSString * imagePath1 = [otaRawImagePath1 stringByReplacingOccurrencesOfString:@"raw-image" withString:@"image"]; + + NSString * imagePath2 = [otaRawImagePath2 stringByReplacingOccurrencesOfString:@"raw-image" withString:@"image"]; + + // 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 * task1 = [[NSTask alloc] init]; + [task1 setLaunchPath:imageToolPath]; + [task1 setArguments:@[ + @"create", @"-v", @"0xFFF1", @"-p", @"0x8001", @"-vn", [kUpdatedSoftwareVersion_10 stringValue], @"-vs", + kUpdatedSoftwareVersionString_10, @"-da", @"sha256", otaRawImagePath1, imagePath1 + ]]; + NSError * launchError = nil; + [task1 launchAndReturnError:&launchError]; + XCTAssertNil(launchError); + [task1 waitUntilExit]; + XCTAssertEqual([task1 terminationStatus], 0); + + NSTask * task2 = [[NSTask alloc] init]; + [task2 setLaunchPath:imageToolPath]; + [task2 setArguments:@[ + @"create", @"-v", @"0xFFF1", @"-p", @"0x8001", @"-vn", [kUpdatedSoftwareVersion_5 stringValue], @"-vs", + kUpdatedSoftwareVersionString_5, @"-da", @"sha256", otaRawImagePath2, imagePath2 + ]]; + launchError = nil; + [task2 launchAndReturnError:&launchError]; + XCTAssertNil(launchError); + [task2 waitUntilExit]; + XCTAssertEqual([task2 terminationStatus], 0); + + NSData * updateToken1 = [sOTAProviderDelegate generateUpdateToken]; + NSData * updateToken2 = [sOTAProviderDelegate generateUpdateToken]; + + __block NSFileHandle * readHandle; + __block uint64_t imageSize; + __block uint32_t lastBlockIndex = UINT32_MAX; + const uint16_t busyDelay = 30; // 30 second + __auto_type * device1 = sConnectedDevice1; + __auto_type * device2 = sConnectedDevice2; + + // This to keep track of whether queryImageHandler for device 2 was called or not. The first time it's called we will + // fulfill queryExpectation2 and proceed with BDX for device 1. + __block bool firstQueryImageForDevice2Received = false; + + // Set up the query handler for device 1 to return image available at imagePath1 + sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, + MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId1)); + XCTAssertEqual(controller, sController); + + sOTAProviderDelegate.queryImageHandler = nil; + [sOTAProviderDelegate respondAvailableWithDelay:@(0) + uri:imagePath1 + updateToken:updateToken1 + softwareVersion:kUpdatedSoftwareVersion_10 + softwareVersionString:kUpdatedSoftwareVersionString_10 + completion:completion]; + [queryExpectation1 fulfill]; + }; + + // Set up the BDX transfer begin, block query and transfer end handlers for device1 so it can go ahead with the BDX transfer + sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSString * fileDesignator, + NSNumber * offset, MTRStatusCompletion outerCompletion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId1)); + XCTAssertEqual(controller, sController); + XCTAssertEqualObjects(fileDesignator, imagePath1); + XCTAssertEqualObjects(offset, @(0)); + + readHandle = [NSFileHandle fileHandleForReadingAtPath:fileDesignator]; + XCTAssertNotNil(readHandle); + + NSError * endSeekError; + XCTAssertTrue([readHandle seekToEndReturningOffset:&imageSize error:&endSeekError]); + XCTAssertNil(endSeekError); + sOTAProviderDelegate.transferBeginHandler = nil; + + // Don't actually respond until the second requestor has queried us for + // an image to ensure BDX for device1 starts only after device2 has queried us once. + + // Set up the query handler for device2 to return image available at imagePath2 but a busy delay of 30 secs + sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, + MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion innerCompletion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId2)); + XCTAssertEqual(controller, sController); + + [sOTAProviderDelegate respondAvailableWithDelay:@(busyDelay) + uri:imagePath2 + updateToken:updateToken2 + softwareVersion:kUpdatedSoftwareVersion_5 + softwareVersionString:kUpdatedSoftwareVersionString_5 + completion:innerCompletion]; + if (!firstQueryImageForDevice2Received) { + [queryExpectation2 fulfill]; + + // Respond with success for the tranfer begin completion for device1 + [sOTAProviderDelegate respondSuccess:outerCompletion]; + [bdxBeginExpectation1 fulfill]; + } + firstQueryImageForDevice2Received = true; + }; + + // Announce ourselves to device2 + announceResponseExpectation2 = [self announceProviderToDevice:device2]; + }; + 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 < imageSize); + + 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) { + [bdxQueryExpectation1 fulfill]; + } + }; + sOTAProviderDelegate.transferEndHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSError * _Nullable error) { + XCTAssertEqualObjects(nodeID, @(kDeviceId1)); + XCTAssertEqual(controller, sController); + XCTAssertNil(error); + sOTAProviderDelegate.transferEndHandler = nil; + [bdxEndExpectation1 fulfill]; + + // BDX transfer with device1 has completed + // Set up the query handler for device2 to return image available at imagePath2 + sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, + MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId2)); + XCTAssertEqual(controller, sController); + + sOTAProviderDelegate.queryImageHandler = nil; + [sOTAProviderDelegate respondAvailableWithDelay:@(0) + uri:imagePath2 + updateToken:updateToken2 + softwareVersion:kUpdatedSoftwareVersion_5 + softwareVersionString:kUpdatedSoftwareVersionString_5 + completion:completion]; + NSLog(@"OTA queryExpectation3"); + [queryExpectation3 fulfill]; + }; + + // Set up the BDX transfer begin, block query and transfer end handlers for device2 so it can go ahead with the BDX transfer + sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, + NSString * fileDesignator, NSNumber * offset, MTRStatusCompletion completion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId2)); + XCTAssertEqual(controller, sController); + XCTAssertEqualObjects(fileDesignator, imagePath2); + 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]; + lastBlockIndex = UINT32_MAX; + [bdxBeginExpectation2 fulfill]; + }; + sOTAProviderDelegate.blockQueryHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSNumber * blockSize, + NSNumber * blockIndex, NSNumber * bytesToSkip, BlockQueryCompletion completion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId2)); + 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 < imageSize); + + 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) { + [bdxQueryExpectation2 fulfill]; + } + }; + sOTAProviderDelegate.transferEndHandler + = ^(NSNumber * nodeID, MTRDeviceController * controller, NSError * _Nullable error) { + XCTAssertEqualObjects(nodeID, @(kDeviceId2)); + XCTAssertEqual(controller, sController); + XCTAssertNil(error); + sOTAProviderDelegate.transferEndHandler = nil; + [bdxEndExpectation2 fulfill]; + }; + }; + + // Flags to track if the applyUpdateRequest from both device1 and device2 has been handled. + __block bool device1HasHandledApplyUpdateRequest = false; + __block bool device2HasHandledApplyUpdateRequest = false; + + // Flags to track if device1 and device have notified that the update has been applied. + __block bool device1HasNotifiedUpdateApplied = false; + __block bool device2HasNotifiedUpdateApplied = false; + + // Set up the apply update request and update applied handlers for device1 and device2. Use the nodeID to verify which + // device sent the applyUpdateRequest and subsequent notifyUpdateApplied commands. + sOTAProviderDelegate.applyUpdateRequestHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, + MTROTASoftwareUpdateProviderClusterApplyUpdateRequestParams * params, ApplyUpdateRequestCompletion completion) { + XCTAssertTrue([nodeID isEqualToNumber:@(kDeviceId1)] || [nodeID isEqualToNumber:@(kDeviceId2)]); + + bool isDeviceID1 = [nodeID isEqualToNumber:@(kDeviceId1)]; + NSData * updateToken = (isDeviceID1) ? updateToken1 : updateToken2; + + // Device1 is updated to version 10 and device2 to version 5. + NSNumber * kSoftwareVersion = (isDeviceID1) ? kUpdatedSoftwareVersion_10 : kUpdatedSoftwareVersion_5; + NSString * otaImageFilePath = (isDeviceID1) ? otaRawImagePath1 : otaRawImagePath2; + NSString * otaDownloadedFilePath = (isDeviceID1) ? kOtaDownloadedFilePath1 : kOtaDownloadedFilePath2; + + XCTAssertEqual(controller, sController); + XCTAssertEqualObjects(params.updateToken, updateToken); + XCTAssertEqualObjects(params.newVersion, kSoftwareVersion); + + XCTAssertTrue([[NSFileManager defaultManager] contentsEqualAtPath:otaImageFilePath andPath:otaDownloadedFilePath]); + + [sOTAProviderDelegate respondToApplyUpdateRequestWithAction:MTROTASoftwareUpdateProviderOTAApplyUpdateActionProceed + completion:completion]; + + if (isDeviceID1) { + device1HasHandledApplyUpdateRequest = true; + [applyUpdateRequestExpectation1 fulfill]; + } else { + device2HasHandledApplyUpdateRequest = true; + [applyUpdateRequestExpectation2 fulfill]; + } + + // If the applyUpdateRequest from both device1 and device2 has been handled, reset the + // applyUpdateRequestHandler to nil + if (device1HasHandledApplyUpdateRequest && device2HasHandledApplyUpdateRequest) { + sOTAProviderDelegate.applyUpdateRequestHandler = nil; + } + }; + + sOTAProviderDelegate.notifyUpdateAppliedHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, + MTROTASoftwareUpdateProviderClusterNotifyUpdateAppliedParams * params, MTRStatusCompletion completion) { + XCTAssertTrue([nodeID isEqualToNumber:@(kDeviceId1)] || [nodeID isEqualToNumber:@(kDeviceId2)]); + + bool isDeviceID1 = [nodeID isEqualToNumber:@(kDeviceId1)]; + NSData * updateToken = (isDeviceID1) ? updateToken1 : updateToken2; + + // Device1 is updated to version 10 and device2 to version 5. + NSNumber * kSoftwareVersion = (isDeviceID1) ? kUpdatedSoftwareVersion_10 : kUpdatedSoftwareVersion_5; + + XCTAssertEqual(controller, sController); + XCTAssertEqualObjects(params.updateToken, updateToken); + XCTAssertEqualObjects(params.softwareVersion, kSoftwareVersion); + + [sOTAProviderDelegate respondSuccess:completion]; + + if (isDeviceID1) { + device1HasNotifiedUpdateApplied = true; + [notifyUpdateAppliedExpectation1 fulfill]; + } else { + device2HasNotifiedUpdateApplied = true; + [notifyUpdateAppliedExpectation2 fulfill]; + } + + // If both device1 and device2 have notified that the update has been applied, reset the + // notifyUpdateAppliedHandler to nil + if (device1HasNotifiedUpdateApplied && device2HasNotifiedUpdateApplied) { + sOTAProviderDelegate.notifyUpdateAppliedHandler = nil; + } + }; + + // Advertise ourselves as an OTA provider. + XCTestExpectation * announceResponseExpectation1 = [self announceProviderToDevice:device1]; + + // Make sure we get our callbacks in order for both device1 and device2. Since we do not + // send image available to device2 until BDX for device1 has ended, queryExpectation3 must follow + // bdxEndExpectation1. + // + // Give it a bit more time, because we want to allow time for the BDX downloads. + [self waitForExpectations:@[ + queryExpectation1, bdxBeginExpectation1, bdxQueryExpectation1, bdxEndExpectation1, queryExpectation3, bdxBeginExpectation2, + bdxQueryExpectation2, bdxEndExpectation2 + ] + timeout:(kTimeoutWithUpdateInSeconds) enforceOrder:YES]; + + // Make sure we get our query expectation for device2 fulfilled when we send busy for device2. + [self waitForExpectations:@[ queryExpectation2 ] timeout:(kTimeoutInSeconds)]; + + // Nothing really defines the ordering of bdxEndExpectation and + // applyUpdateRequestExpectation with respect to each other. + [self waitForExpectations:@[ + applyUpdateRequestExpectation1, notifyUpdateAppliedExpectation1, applyUpdateRequestExpectation2, + notifyUpdateAppliedExpectation2 + ] + timeout:kTimeoutInSeconds + enforceOrder:NO]; + + // Nothing defines the ordering of announceResponseExpectation with respect + // to _any_ of the above expectations. + [self waitForExpectations:@[ announceResponseExpectation1, announceResponseExpectation2 ] timeout:kTimeoutInSeconds]; +} + +- (void)test007_DoBDXTransferIncrementalOtaUpdate +{ + // In this test, we test incremental OTA update with device3. First we update device3 + // to version 5 using image at imagePath1. Once device3 updates and reboots, we update + // it to version 10 using image at imagePath2. + // + // 1) We announce ourselves to device3. + // 2) When device3 queries, we return image available at imagePath1 with version number 5. + // 3) We set the BDX begin and end handlers for device3 and device3 starts the BDX transfer. + // 4) Device3 completes the BDX transfer + // 5) Device3 applies the update and reboots with the new image with version number 5 + // 6) We announce ourselves to device 3 again. + // 7) When device3 queries again, we return image available at imagePath2 with version number 10. + // 8) We set the BDX begin and end handlers for device3 and device3 starts the BDX transfer. + // 4) Device3 completes the BDX transfer + // 5) Device3 applies the update and reboots with the new image with version number 10 + + // This test expects a pre-generated raw image at otaRawImagePath1 and a raw image at otaRawImagePath2 + NSString * otaRawImagePath1 = @"/tmp/ota-raw-image-v5"; + NSString * otaRawImagePath2 = @"/tmp/ota-raw-image-v10"; + + // Check whether the ota raw image exists at otaRawImagePath1 and otaRawImagePath2 + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:otaRawImagePath1]); + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:otaRawImagePath2]); + + __auto_type * device = sConnectedDevice3; + + __auto_type * checker = + [[MTROTAProviderTransferChecker alloc] initWithRawImagePath:otaRawImagePath1 + otaImageDownloadFilePath:kOtaDownloadedFilePath3 + nodeID:@(kDeviceId3) + softwareVersion:kUpdatedSoftwareVersion_5 + softwareVersionString:kUpdatedSoftwareVersionString_5 applyUpdateAction:MTROTASoftwareUpdateProviderOTAApplyUpdateActionProceed testcase:self]; @@ -887,8 +1381,39 @@ - (void)test005_DoBDXTransferAllowUpdateRequest // Nothing defines the ordering of announceResponseExpectation with respect // to _any_ of the above expectations. [self waitForExpectations:@[ announceResponseExpectation ] timeout:kTimeoutInSeconds]; + + // Provide an incremental update and makes sure the app is updated to the new version + + __auto_type * checker1 = + [[MTROTAProviderTransferChecker alloc] initWithRawImagePath:otaRawImagePath2 + otaImageDownloadFilePath:kOtaDownloadedFilePath3 + nodeID:@(kDeviceId3) + softwareVersion:kUpdatedSoftwareVersion_10 + softwareVersionString:kUpdatedSoftwareVersionString_10 + applyUpdateAction:MTROTASoftwareUpdateProviderOTAApplyUpdateActionProceed + testcase:self]; + + // Advertise ourselves as an OTA provider. + XCTestExpectation * announceResponseExpectation1 = [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:@[ + checker1.queryExpectation, checker1.bdxBeginExpectation, checker1.bdxQueryExpectation, checker1.bdxEndExpectation + ] + timeout:(kTimeoutWithUpdateInSeconds) enforceOrder:YES]; + + // Nothing really defines the ordering of bdxEndExpectation and + // applyUpdateRequestExpectation with respect to each other. + [self waitForExpectations:@[ checker1.applyUpdateRequestExpectation, checker1.notifyUpdateAppliedExpectation ] + timeout:kTimeoutInSeconds + enforceOrder:YES]; + + // Nothing defines the ordering of announceResponseExpectation with respect + // to _any_ of the above expectations. + [self waitForExpectations:@[ announceResponseExpectation1 ] timeout:kTimeoutInSeconds]; } -#endif // ENABLE_TEST_005 +#endif // ENABLE_REAL_OTA_UPDATE_TESTS - (void)test999_TearDown { @@ -897,6 +1422,10 @@ - (void)test999_TearDown device = [MTRBaseDevice deviceWithNodeID:@(kDeviceId2) controller:sController]; ResetCommissionee(device, dispatch_get_main_queue(), self, kTimeoutInSeconds); + + device = [MTRBaseDevice deviceWithNodeID:@(kDeviceId3) controller:sController]; + ResetCommissionee(device, dispatch_get_main_queue(), self, kTimeoutInSeconds); + [[self class] shutdownStack]; }