Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an OTA download test to Darwin framework CI. #25951

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/darwin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
224 changes: 214 additions & 10 deletions src/darwin/Framework/CHIPTests/MTROTAProviderTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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];
}
}
Expand All @@ -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];
}
}
Expand All @@ -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];
}
}
Expand All @@ -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];
}
}
Expand All @@ -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);
}
}
Expand All @@ -192,6 +198,8 @@ - (void)handleBDXTransferSessionEndForNodeID:(NSNumber *)nodeID
{
if (self.transferEndHandler) {
self.transferEndHandler(nodeID, controller, error);
} else {
XCTFail(@"Unexpected end of BDX transfer");
}
}

Expand All @@ -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);
Expand All @@ -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);
}

Expand All @@ -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];
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -516,19 +550,18 @@ - (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);

// 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];
};
Expand Down Expand Up @@ -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];
Expand Down