Skip to content

Commit 1230074

Browse files
bzbarsky-applepull[bot]
authored andcommitted
Add CI for darwin-framework-tool acting as OTA provider. (#25851)
1 parent 2888cd4 commit 1230074

File tree

7 files changed

+221
-9
lines changed

7 files changed

+221
-9
lines changed

.github/workflows/darwin-tests.yaml

+13
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,19 @@ jobs:
121121
--tv-app ./out/darwin-x64-tv-app-${BUILD_VARIANT}/chip-tv-app \
122122
--bridge-app ./out/darwin-x64-bridge-${BUILD_VARIANT}/chip-bridge-app \
123123
"
124+
- name: Run OTA Test
125+
timeout-minutes: 5
126+
run: |
127+
./scripts/run_in_build_env.sh \
128+
"./scripts/tests/run_darwin_framework_ota_test.py \
129+
run \
130+
--darwin-framework-tool ./out/darwin-x64-darwin-framework-tool-${BUILD_VARIANT}/darwin-framework-tool \
131+
--ota-requestor-app ./out/darwin-x64-ota-requestor-${BUILD_VARIANT}/chip-ota-requestor-app \
132+
--ota-data-file /tmp/rawImage \
133+
--ota-image-file /tmp/otaImage \
134+
--ota-destination-file /tmp/downloadedImage \
135+
--ota-candidate-file /tmp/otaCandidateJSON \
136+
"
124137
- name: Uploading core files
125138
uses: actions/upload-artifact@v3
126139
if: ${{ failure() && !env.ACT }}

examples/darwin-framework-tool/commands/interactive/InteractiveCommands.h

+7-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ class Commands;
3030
class InteractiveStartCommand : public CHIPCommandBridge
3131
{
3232
public:
33-
InteractiveStartCommand(Commands * commandsHandler) : CHIPCommandBridge("start"), mHandler(commandsHandler) {}
33+
InteractiveStartCommand(Commands * commandsHandler) : CHIPCommandBridge("start"), mHandler(commandsHandler)
34+
{
35+
AddArgument(
36+
"additional-prompt", &mAdditionalPrompt,
37+
"Force printing of an additional prompt that can then be detected by something trying to script interactive mode");
38+
}
3439

3540
CHIP_ERROR RunCommand() override;
3641

@@ -39,4 +44,5 @@ class InteractiveStartCommand : public CHIPCommandBridge
3944
private:
4045
bool ParseCommand(char * command);
4146
Commands * mHandler = nullptr;
47+
chip::Optional<char *> mAdditionalPrompt;
4248
};

examples/darwin-framework-tool/commands/interactive/InteractiveCommands.mm

+7-2
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,18 @@ void ENFORCE_FORMAT(3, 0) LoggingCallback(const char * module, uint8_t category,
7373
}
7474
} // namespace
7575

76-
char * GetCommand(char * command)
76+
char * GetCommand(const chip::Optional<char *> & mAdditionalPrompt, char * command)
7777
{
7878
if (command != nullptr) {
7979
free(command);
8080
command = nullptr;
8181
}
8282

83+
if (mAdditionalPrompt.HasValue()) {
84+
ClearLine();
85+
printf("%s\n", mAdditionalPrompt.Value());
86+
ClearLine();
87+
}
8388
command = readline(kInteractiveModePrompt);
8489

8590
// Do not save empty lines
@@ -118,7 +123,7 @@ el_status_t StopFunction()
118123

119124
char * command = nullptr;
120125
while (YES) {
121-
command = GetCommand(command);
126+
command = GetCommand(mAdditionalPrompt, command);
122127
if (command != nullptr && !ParseCommand(command)) {
123128
break;
124129
}

examples/darwin-framework-tool/commands/pairing/GetCommissionerNodeIdCommand.mm

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
VerifyOrReturnError(nil != controller, CHIP_ERROR_INCORRECT_STATE);
2727

2828
auto id = [controller.controllerNodeId unsignedLongLongValue];
29-
ChipLogProgress(chipTool, "Commissioner Node Id 0x:" ChipLogFormatX64, ChipLogValueX64(id));
29+
ChipLogProgress(chipTool, "Commissioner Node Id 0x" ChipLogFormatX64, ChipLogValueX64(id));
3030

3131
SetCommandExitStatus(CHIP_NO_ERROR);
3232
return CHIP_NO_ERROR;

scripts/tests/chiptest/accessories.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,14 @@ def waitForMessage(self, name, message):
106106
return accessory.waitForMessage(' '.join(message))
107107
return False
108108

109-
def createOtaImage(self, otaImageFilePath, rawImageFilePath, rawImageContent):
109+
def createOtaImage(self, otaImageFilePath, rawImageFilePath, rawImageContent, vid='0xDEAD', pid='0xBEEF'):
110110
# Write the raw image content
111111
with open(rawImageFilePath, 'w') as rawFile:
112112
rawFile.write(rawImageContent)
113113

114114
# Add an OTA header to the raw file
115115
otaImageTool = _DEFAULT_CHIP_ROOT + '/src/app/ota_image_tool.py'
116-
cmd = [otaImageTool, 'create', '-v', '0xDEAD', '-p', '0xBEEF', '-vn', '2',
116+
cmd = [otaImageTool, 'create', '-v', vid, '-p', pid, '-vn', '2',
117117
'-vs', "2.0", '-da', 'sha256', rawImageFilePath, otaImageFilePath]
118118
s = subprocess.Popen(cmd)
119119
s.wait()

scripts/tests/chiptest/runner.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ class Runner:
123123
def __init__(self, capture_delegate=None):
124124
self.capture_delegate = capture_delegate
125125

126-
def RunSubprocess(self, cmd, name, wait=True, dependencies=[], timeout_seconds: typing.Optional[int] = None):
126+
def RunSubprocess(self, cmd, name, wait=True, dependencies=[], timeout_seconds: typing.Optional[int] = None, stdin=None):
127127
outpipe = LogPipe(
128128
logging.DEBUG, capture_delegate=self.capture_delegate,
129129
name=name + ' OUT')
@@ -133,12 +133,12 @@ def RunSubprocess(self, cmd, name, wait=True, dependencies=[], timeout_seconds:
133133

134134
if sys.platform == 'darwin':
135135
# Try harder to avoid any stdout buffering in our tests
136-
cmd = ['stdbuf', '-o0'] + cmd
136+
cmd = ['stdbuf', '-o0', '-i0'] + cmd
137137

138138
if self.capture_delegate:
139139
self.capture_delegate.Log(name, 'EXECUTING %r' % cmd)
140140

141-
s = subprocess.Popen(cmd, stdout=outpipe, stderr=errpipe)
141+
s = subprocess.Popen(cmd, stdin=stdin, stdout=outpipe, stderr=errpipe)
142142
outpipe.close()
143143
errpipe.close()
144144

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
#! /usr/bin/env -S python3 -B
2+
3+
import io
4+
import json
5+
import logging
6+
import time
7+
from subprocess import PIPE
8+
9+
import click
10+
from chiptest.accessories import AppsRegister
11+
from chiptest.runner import Runner
12+
from chiptest.test_definition import App, ExecutionCapture
13+
from yaml.paths_finder import PathsFinder
14+
15+
TEST_NODE_ID = '0x12344321'
16+
TEST_VID = '0xFFF1'
17+
TEST_PID = '0x8001'
18+
19+
20+
class DarwinToolRunner:
21+
def __init__(self, runner, command):
22+
self.process = None
23+
self.outpipe = None
24+
self.runner = runner
25+
self.lastLogIndex = 0
26+
self.command = command
27+
self.stdin = None
28+
29+
def start(self):
30+
self.process, self.outpipe, errpipe = self.runner.RunSubprocess(self.command,
31+
name='DARWIN-TOOL',
32+
wait=False,
33+
stdin=PIPE)
34+
self.stdin = io.TextIOWrapper(self.process.stdin, line_buffering=True)
35+
36+
def stop(self):
37+
if self.process:
38+
self.process.kill()
39+
40+
def waitForMessage(self, message):
41+
logging.debug('Waiting for %s' % message)
42+
43+
start_time = time.monotonic()
44+
ready, self.lastLogIndex = self.outpipe.CapturedLogContains(
45+
message, self.lastLogIndex)
46+
while not ready:
47+
if self.process.poll() is not None:
48+
died_str = ('Process died while waiting for %s, returncode %d' %
49+
(message, self.process.returncode))
50+
logging.error(died_str)
51+
raise Exception(died_str)
52+
if time.monotonic() - start_time > 10:
53+
raise Exception('Timeout while waiting for %s' % message)
54+
time.sleep(0.1)
55+
ready, self.lastLogIndex = self.outpipe.CapturedLogContains(
56+
message, self.lastLogIndex)
57+
58+
logging.debug('Success waiting for: %s' % message)
59+
60+
61+
class InteractiveDarwinTool(DarwinToolRunner):
62+
def __init__(self, runner, binary_path):
63+
self.prompt = "WAITING FOR COMMANDS NOW"
64+
super().__init__(runner, [binary_path, "interactive", "start", "--additional-prompt", self.prompt])
65+
66+
def waitForPrompt(self):
67+
self.waitForMessage(self.prompt)
68+
69+
def sendCommand(self, command):
70+
logging.debug('Sending command %s' % command)
71+
print(command, file=self.stdin)
72+
self.waitForPrompt()
73+
74+
75+
@click.group(chain=True)
76+
@click.pass_context
77+
def main(context):
78+
pass
79+
80+
81+
@main.command(
82+
'run', help='Execute the test')
83+
@click.option(
84+
'--darwin-framework-tool',
85+
help="what darwin-framework-tool to use")
86+
@click.option(
87+
'--ota-requestor-app',
88+
help='what ota requestor app to use')
89+
@click.option(
90+
'--ota-data-file',
91+
required=True,
92+
help='The file to use to store our OTA data. This file does not need to exist.')
93+
@click.option(
94+
'--ota-image-file',
95+
required=True,
96+
help='The file to use to store the OTA image we plan to send. This file does not need to exist.')
97+
@click.option(
98+
'--ota-destination-file',
99+
required=True,
100+
help='The destination file to use for the requestor\'s download. This file does not need to exist.')
101+
@click.option(
102+
'--ota-candidate-file',
103+
required=True,
104+
help='The file to use for our OTA candidate JSON. This file does not need to exist.')
105+
@click.pass_context
106+
def cmd_run(context, darwin_framework_tool, ota_requestor_app, ota_data_file, ota_image_file, ota_destination_file, ota_candidate_file):
107+
paths_finder = PathsFinder()
108+
109+
if darwin_framework_tool is None:
110+
darwin_framework_tool = paths_finder.get('darwin-framework-tool')
111+
if ota_requestor_app is None:
112+
ota_requestor_app = paths_finder.get('chip-ota-requestor-app')
113+
114+
runner = Runner()
115+
runner.capture_delegate = ExecutionCapture()
116+
117+
apps_register = AppsRegister()
118+
apps_register.init()
119+
120+
darwin_tool = None
121+
122+
try:
123+
apps_register.createOtaImage(ota_image_file, ota_data_file, "This is some test OTA data", vid=TEST_VID, pid=TEST_PID)
124+
json_data = {
125+
"deviceSoftwareVersionModel": [{
126+
"vendorId": int(TEST_VID, 16),
127+
"productId": int(TEST_PID, 16),
128+
"softwareVersion": 2,
129+
"softwareVersionString": "2.0",
130+
"cDVersionNumber": 18,
131+
"softwareVersionValid": True,
132+
"minApplicableSoftwareVersion": 0,
133+
"maxApplicableSoftwareVersion": 100,
134+
"otaURL": ota_image_file
135+
}]
136+
}
137+
with open(ota_candidate_file, "w") as f:
138+
json.dump(json_data, f)
139+
140+
requestor_app = App(runner, [ota_requestor_app, '--otaDownloadPath', ota_destination_file])
141+
apps_register.add('default', requestor_app)
142+
143+
requestor_app.start()
144+
145+
pairing_cmd = [darwin_framework_tool, 'pairing', 'code', TEST_NODE_ID, requestor_app.setupCode]
146+
runner.RunSubprocess(pairing_cmd, name='PAIR', dependencies=[apps_register])
147+
148+
# pairing get-commissioner-node-id does not seem to work right in interactive mode for some reason
149+
darwin_tool = DarwinToolRunner(runner, [darwin_framework_tool, 'pairing', 'get-commissioner-node-id'])
150+
darwin_tool.start()
151+
darwin_tool.waitForMessage(": Commissioner Node Id")
152+
nodeIdLine = darwin_tool.outpipe.FindLastMatchingLine('.*: Commissioner Node Id (0x[0-9A-F]+)')
153+
if not nodeIdLine:
154+
raise Exception("Unable to find commissioner node id")
155+
commissionerNodeId = nodeIdLine.group(1)
156+
darwin_tool.stop()
157+
158+
darwin_tool = InteractiveDarwinTool(runner, darwin_framework_tool)
159+
darwin_tool.start()
160+
161+
darwin_tool.waitForPrompt()
162+
163+
darwin_tool.sendCommand("otasoftwareupdateapp candidate-file-path %s" % ota_candidate_file)
164+
darwin_tool.sendCommand("otasoftwareupdateapp set-reply-params --status 0")
165+
darwin_tool.sendCommand("otasoftwareupdaterequestor announce-otaprovider %s 0 0 0 %s 0" %
166+
(commissionerNodeId, TEST_NODE_ID))
167+
168+
# Now wait for the OTA download to finish.
169+
requestor_app.waitForMessage("OTA image downloaded to %s" % ota_destination_file)
170+
171+
# Make sure the right thing was downloaded.
172+
apps_register.compareFiles(ota_data_file, ota_destination_file)
173+
174+
except Exception:
175+
logging.error("!!!!!!!!!!!!!!!!!!!! ERROR !!!!!!!!!!!!!!!!!!!!!!")
176+
runner.capture_delegate.LogContents()
177+
raise
178+
finally:
179+
if darwin_tool is not None:
180+
darwin_tool.stop()
181+
apps_register.killAll()
182+
apps_register.factoryResetAll()
183+
apps_register.removeAll()
184+
apps_register.uninit()
185+
186+
187+
if __name__ == '__main__':
188+
main(auto_envvar_prefix='CHIP')

0 commit comments

Comments
 (0)