From 8a5b96cd2d004024dbf7de1f23cd709adc509c54 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Tue, 28 Mar 2023 11:32:08 -0400 Subject: [PATCH] Add CI for darwin-framework-tool acting as OTA provider. (#25851) --- .github/workflows/darwin-tests.yaml | 13 ++ .../interactive/InteractiveCommands.h | 8 +- .../interactive/InteractiveCommands.mm | 9 +- .../pairing/GetCommissionerNodeIdCommand.mm | 2 +- scripts/tests/chiptest/accessories.py | 4 +- scripts/tests/chiptest/runner.py | 6 +- .../tests/run_darwin_framework_ota_test.py | 188 ++++++++++++++++++ 7 files changed, 221 insertions(+), 9 deletions(-) create mode 100755 scripts/tests/run_darwin_framework_ota_test.py diff --git a/.github/workflows/darwin-tests.yaml b/.github/workflows/darwin-tests.yaml index a0c8642d557d04..cdf8b5a4364572 100644 --- a/.github/workflows/darwin-tests.yaml +++ b/.github/workflows/darwin-tests.yaml @@ -121,6 +121,19 @@ jobs: --tv-app ./out/darwin-x64-tv-app-${BUILD_VARIANT}/chip-tv-app \ --bridge-app ./out/darwin-x64-bridge-${BUILD_VARIANT}/chip-bridge-app \ " + - name: Run OTA Test + timeout-minutes: 5 + run: | + ./scripts/run_in_build_env.sh \ + "./scripts/tests/run_darwin_framework_ota_test.py \ + run \ + --darwin-framework-tool ./out/darwin-x64-darwin-framework-tool-${BUILD_VARIANT}/darwin-framework-tool \ + --ota-requestor-app ./out/darwin-x64-ota-requestor-${BUILD_VARIANT}/chip-ota-requestor-app \ + --ota-data-file /tmp/rawImage \ + --ota-image-file /tmp/otaImage \ + --ota-destination-file /tmp/downloadedImage \ + --ota-candidate-file /tmp/otaCandidateJSON \ + " - name: Uploading core files uses: actions/upload-artifact@v3 if: ${{ failure() && !env.ACT }} diff --git a/examples/darwin-framework-tool/commands/interactive/InteractiveCommands.h b/examples/darwin-framework-tool/commands/interactive/InteractiveCommands.h index c6e8c5d85e45f1..cff524a1c0650e 100644 --- a/examples/darwin-framework-tool/commands/interactive/InteractiveCommands.h +++ b/examples/darwin-framework-tool/commands/interactive/InteractiveCommands.h @@ -30,7 +30,12 @@ class Commands; class InteractiveStartCommand : public CHIPCommandBridge { public: - InteractiveStartCommand(Commands * commandsHandler) : CHIPCommandBridge("start"), mHandler(commandsHandler) {} + InteractiveStartCommand(Commands * commandsHandler) : CHIPCommandBridge("start"), mHandler(commandsHandler) + { + AddArgument( + "additional-prompt", &mAdditionalPrompt, + "Force printing of an additional prompt that can then be detected by something trying to script interactive mode"); + } CHIP_ERROR RunCommand() override; @@ -39,4 +44,5 @@ class InteractiveStartCommand : public CHIPCommandBridge private: bool ParseCommand(char * command); Commands * mHandler = nullptr; + chip::Optional mAdditionalPrompt; }; diff --git a/examples/darwin-framework-tool/commands/interactive/InteractiveCommands.mm b/examples/darwin-framework-tool/commands/interactive/InteractiveCommands.mm index 4188da365574c5..d0c900c3271ddd 100644 --- a/examples/darwin-framework-tool/commands/interactive/InteractiveCommands.mm +++ b/examples/darwin-framework-tool/commands/interactive/InteractiveCommands.mm @@ -73,13 +73,18 @@ void ENFORCE_FORMAT(3, 0) LoggingCallback(const char * module, uint8_t category, } } // namespace -char * GetCommand(char * command) +char * GetCommand(const chip::Optional & mAdditionalPrompt, char * command) { if (command != nullptr) { free(command); command = nullptr; } + if (mAdditionalPrompt.HasValue()) { + ClearLine(); + printf("%s\n", mAdditionalPrompt.Value()); + ClearLine(); + } command = readline(kInteractiveModePrompt); // Do not save empty lines @@ -118,7 +123,7 @@ el_status_t StopFunction() char * command = nullptr; while (YES) { - command = GetCommand(command); + command = GetCommand(mAdditionalPrompt, command); if (command != nullptr && !ParseCommand(command)) { break; } diff --git a/examples/darwin-framework-tool/commands/pairing/GetCommissionerNodeIdCommand.mm b/examples/darwin-framework-tool/commands/pairing/GetCommissionerNodeIdCommand.mm index 48502f84641b58..115f7b0821c048 100644 --- a/examples/darwin-framework-tool/commands/pairing/GetCommissionerNodeIdCommand.mm +++ b/examples/darwin-framework-tool/commands/pairing/GetCommissionerNodeIdCommand.mm @@ -26,7 +26,7 @@ VerifyOrReturnError(nil != controller, CHIP_ERROR_INCORRECT_STATE); auto id = [controller.controllerNodeId unsignedLongLongValue]; - ChipLogProgress(chipTool, "Commissioner Node Id 0x:" ChipLogFormatX64, ChipLogValueX64(id)); + ChipLogProgress(chipTool, "Commissioner Node Id 0x" ChipLogFormatX64, ChipLogValueX64(id)); SetCommandExitStatus(CHIP_NO_ERROR); return CHIP_NO_ERROR; diff --git a/scripts/tests/chiptest/accessories.py b/scripts/tests/chiptest/accessories.py index c3641d641171fa..d8ca1c02945cc9 100644 --- a/scripts/tests/chiptest/accessories.py +++ b/scripts/tests/chiptest/accessories.py @@ -106,14 +106,14 @@ def waitForMessage(self, name, message): return accessory.waitForMessage(' '.join(message)) return False - def createOtaImage(self, otaImageFilePath, rawImageFilePath, rawImageContent): + def createOtaImage(self, otaImageFilePath, rawImageFilePath, rawImageContent, vid='0xDEAD', pid='0xBEEF'): # Write the raw image content with open(rawImageFilePath, 'w') as rawFile: rawFile.write(rawImageContent) # Add an OTA header to the raw file otaImageTool = _DEFAULT_CHIP_ROOT + '/src/app/ota_image_tool.py' - cmd = [otaImageTool, 'create', '-v', '0xDEAD', '-p', '0xBEEF', '-vn', '2', + cmd = [otaImageTool, 'create', '-v', vid, '-p', pid, '-vn', '2', '-vs', "2.0", '-da', 'sha256', rawImageFilePath, otaImageFilePath] s = subprocess.Popen(cmd) s.wait() diff --git a/scripts/tests/chiptest/runner.py b/scripts/tests/chiptest/runner.py index d2f3d96503210e..fdba938224d45e 100644 --- a/scripts/tests/chiptest/runner.py +++ b/scripts/tests/chiptest/runner.py @@ -123,7 +123,7 @@ class Runner: def __init__(self, capture_delegate=None): self.capture_delegate = capture_delegate - def RunSubprocess(self, cmd, name, wait=True, dependencies=[], timeout_seconds: typing.Optional[int] = None): + def RunSubprocess(self, cmd, name, wait=True, dependencies=[], timeout_seconds: typing.Optional[int] = None, stdin=None): outpipe = LogPipe( logging.DEBUG, capture_delegate=self.capture_delegate, name=name + ' OUT') @@ -133,12 +133,12 @@ def RunSubprocess(self, cmd, name, wait=True, dependencies=[], timeout_seconds: if sys.platform == 'darwin': # Try harder to avoid any stdout buffering in our tests - cmd = ['stdbuf', '-o0'] + cmd + cmd = ['stdbuf', '-o0', '-i0'] + cmd if self.capture_delegate: self.capture_delegate.Log(name, 'EXECUTING %r' % cmd) - s = subprocess.Popen(cmd, stdout=outpipe, stderr=errpipe) + s = subprocess.Popen(cmd, stdin=stdin, stdout=outpipe, stderr=errpipe) outpipe.close() errpipe.close() diff --git a/scripts/tests/run_darwin_framework_ota_test.py b/scripts/tests/run_darwin_framework_ota_test.py new file mode 100755 index 00000000000000..672b26369d3c3e --- /dev/null +++ b/scripts/tests/run_darwin_framework_ota_test.py @@ -0,0 +1,188 @@ +#! /usr/bin/env -S python3 -B + +import io +import json +import logging +import time +from subprocess import PIPE + +import click +from chiptest.accessories import AppsRegister +from chiptest.runner import Runner +from chiptest.test_definition import App, ExecutionCapture +from yaml.paths_finder import PathsFinder + +TEST_NODE_ID = '0x12344321' +TEST_VID = '0xFFF1' +TEST_PID = '0x8001' + + +class DarwinToolRunner: + def __init__(self, runner, command): + self.process = None + self.outpipe = None + self.runner = runner + self.lastLogIndex = 0 + self.command = command + self.stdin = None + + def start(self): + self.process, self.outpipe, errpipe = self.runner.RunSubprocess(self.command, + name='DARWIN-TOOL', + wait=False, + stdin=PIPE) + self.stdin = io.TextIOWrapper(self.process.stdin, line_buffering=True) + + def stop(self): + if self.process: + self.process.kill() + + def waitForMessage(self, message): + logging.debug('Waiting for %s' % message) + + start_time = time.monotonic() + ready, self.lastLogIndex = self.outpipe.CapturedLogContains( + message, self.lastLogIndex) + while not ready: + if self.process.poll() is not None: + died_str = ('Process died while waiting for %s, returncode %d' % + (message, self.process.returncode)) + logging.error(died_str) + raise Exception(died_str) + if time.monotonic() - start_time > 10: + raise Exception('Timeout while waiting for %s' % message) + time.sleep(0.1) + ready, self.lastLogIndex = self.outpipe.CapturedLogContains( + message, self.lastLogIndex) + + logging.debug('Success waiting for: %s' % message) + + +class InteractiveDarwinTool(DarwinToolRunner): + def __init__(self, runner, binary_path): + self.prompt = "WAITING FOR COMMANDS NOW" + super().__init__(runner, [binary_path, "interactive", "start", "--additional-prompt", self.prompt]) + + def waitForPrompt(self): + self.waitForMessage(self.prompt) + + def sendCommand(self, command): + logging.debug('Sending command %s' % command) + print(command, file=self.stdin) + self.waitForPrompt() + + +@click.group(chain=True) +@click.pass_context +def main(context): + pass + + +@main.command( + 'run', help='Execute the test') +@click.option( + '--darwin-framework-tool', + help="what darwin-framework-tool to use") +@click.option( + '--ota-requestor-app', + help='what ota requestor app to use') +@click.option( + '--ota-data-file', + required=True, + help='The file to use to store our OTA data. This file does not need to exist.') +@click.option( + '--ota-image-file', + required=True, + help='The file to use to store the OTA image we plan to send. This file does not need to exist.') +@click.option( + '--ota-destination-file', + required=True, + help='The destination file to use for the requestor\'s download. This file does not need to exist.') +@click.option( + '--ota-candidate-file', + required=True, + help='The file to use for our OTA candidate JSON. This file does not need to exist.') +@click.pass_context +def cmd_run(context, darwin_framework_tool, ota_requestor_app, ota_data_file, ota_image_file, ota_destination_file, ota_candidate_file): + paths_finder = PathsFinder() + + if darwin_framework_tool is None: + darwin_framework_tool = paths_finder.get('darwin-framework-tool') + if ota_requestor_app is None: + ota_requestor_app = paths_finder.get('chip-ota-requestor-app') + + runner = Runner() + runner.capture_delegate = ExecutionCapture() + + apps_register = AppsRegister() + apps_register.init() + + darwin_tool = None + + try: + apps_register.createOtaImage(ota_image_file, ota_data_file, "This is some test OTA data", vid=TEST_VID, pid=TEST_PID) + json_data = { + "deviceSoftwareVersionModel": [{ + "vendorId": int(TEST_VID, 16), + "productId": int(TEST_PID, 16), + "softwareVersion": 2, + "softwareVersionString": "2.0", + "cDVersionNumber": 18, + "softwareVersionValid": True, + "minApplicableSoftwareVersion": 0, + "maxApplicableSoftwareVersion": 100, + "otaURL": ota_image_file + }] + } + with open(ota_candidate_file, "w") as f: + json.dump(json_data, f) + + requestor_app = App(runner, [ota_requestor_app, '--otaDownloadPath', ota_destination_file]) + apps_register.add('default', requestor_app) + + requestor_app.start() + + pairing_cmd = [darwin_framework_tool, 'pairing', 'code', TEST_NODE_ID, requestor_app.setupCode] + runner.RunSubprocess(pairing_cmd, name='PAIR', dependencies=[apps_register]) + + # pairing get-commissioner-node-id does not seem to work right in interactive mode for some reason + darwin_tool = DarwinToolRunner(runner, [darwin_framework_tool, 'pairing', 'get-commissioner-node-id']) + darwin_tool.start() + darwin_tool.waitForMessage(": Commissioner Node Id") + nodeIdLine = darwin_tool.outpipe.FindLastMatchingLine('.*: Commissioner Node Id (0x[0-9A-F]+)') + if not nodeIdLine: + raise Exception("Unable to find commissioner node id") + commissionerNodeId = nodeIdLine.group(1) + darwin_tool.stop() + + darwin_tool = InteractiveDarwinTool(runner, darwin_framework_tool) + darwin_tool.start() + + darwin_tool.waitForPrompt() + + darwin_tool.sendCommand("otasoftwareupdateapp candidate-file-path %s" % ota_candidate_file) + darwin_tool.sendCommand("otasoftwareupdateapp set-reply-params --status 0") + darwin_tool.sendCommand("otasoftwareupdaterequestor announce-otaprovider %s 0 0 0 %s 0" % + (commissionerNodeId, TEST_NODE_ID)) + + # Now wait for the OTA download to finish. + requestor_app.waitForMessage("OTA image downloaded to %s" % ota_destination_file) + + # Make sure the right thing was downloaded. + apps_register.compareFiles(ota_data_file, ota_destination_file) + + except Exception: + logging.error("!!!!!!!!!!!!!!!!!!!! ERROR !!!!!!!!!!!!!!!!!!!!!!") + runner.capture_delegate.LogContents() + raise + finally: + if darwin_tool is not None: + darwin_tool.stop() + apps_register.killAll() + apps_register.factoryResetAll() + apps_register.removeAll() + apps_register.uninit() + + +if __name__ == '__main__': + main(auto_envvar_prefix='CHIP')