Skip to content

Commit adfc0c4

Browse files
committed
add extra firmware tests
Change-type: patch Signed-off-by: Yann CARDAILLAC <[email protected]>
1 parent 0f9c042 commit adfc0c4

File tree

2 files changed

+243
-0
lines changed

2 files changed

+243
-0
lines changed

tests/suites/os/suite.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,5 +477,6 @@ module.exports = {
477477
'./tests/internet-sharing',
478478
'./tests/safe-reboot',
479479
'./tests/disk-watchdog',
480+
'./tests/extra-firmware',
480481
],
481482
};
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/* Copyright 2019 balena
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
'use strict';
17+
18+
const VOLUME_NAME = 'test_extra_firmware';
19+
const SYSFS_PATH = '/sys/module/firmware_class/parameters/path';
20+
const CONFIG_PATH = '/mnt/boot/config.json';
21+
const CONFIG_BACKUP_PATH = '/mnt/state/config.json.extra-firmware-test-backup';
22+
23+
const backupConfig = async (context, link, test) => {
24+
test.comment('Backing up config.json...');
25+
await context
26+
.get()
27+
.worker.executeCommandInHostOS(
28+
`cp ${CONFIG_PATH} ${CONFIG_BACKUP_PATH}`,
29+
link,
30+
);
31+
test.comment(`config.json backed up to ${CONFIG_BACKUP_PATH}`);
32+
};
33+
34+
const restoreConfig = async (context, link, test) => {
35+
test.comment('Restoring config.json...');
36+
await context
37+
.get()
38+
.worker.executeCommandInHostOS(
39+
`mv ${CONFIG_BACKUP_PATH} ${CONFIG_PATH}`,
40+
link,
41+
);
42+
test.comment('config.json restored');
43+
};
44+
45+
const createTestVolume = async (context, link, test) => {
46+
test.comment('Creating test docker volume...');
47+
48+
// Create volume
49+
await context
50+
.get()
51+
.worker.executeCommandInHostOS(
52+
`balena volume create ${VOLUME_NAME} || true`,
53+
link,
54+
);
55+
56+
// Verify volume exists
57+
const volumeExists = await context
58+
.get()
59+
.worker.executeCommandInHostOS(
60+
`balena volume inspect ${VOLUME_NAME} >/dev/null 2>&1 && echo exists || echo missing`,
61+
link,
62+
);
63+
test.is(volumeExists.trim(), 'exists', `Docker volume ${VOLUME_NAME} should exist`);
64+
65+
test.comment(`Volume ${VOLUME_NAME} created`);
66+
};
67+
68+
const configureExtraFirmwareAndVerifyServiceTriggered = async (context, link, test) => {
69+
test.comment('Configuring os.kernel.extraFirmwareVol in config.json...');
70+
71+
// Get the current monotonic time (in microseconds) before making the config change
72+
const monotonicTimeBefore = await context
73+
.get()
74+
.worker.executeCommandInHostOS(
75+
`cat /proc/uptime | awk '{printf "%.0f", $1 * 1000000}'`,
76+
link,
77+
);
78+
test.comment(`Monotonic time before config change: ${monotonicTimeBefore.trim()}`);
79+
80+
// Update config.json with the extra firmware volume
81+
await context
82+
.get()
83+
.worker.executeCommandInHostOS(
84+
`jq '.os.kernel.extraFirmwareVol = "${VOLUME_NAME}/_data"' ${CONFIG_PATH} > /tmp/config.json && cp /tmp/config.json ${CONFIG_PATH}`,
85+
link,
86+
);
87+
88+
// Verify the config was set
89+
const configValue = await context
90+
.get()
91+
.worker.executeCommandInHostOS(
92+
`jq -r '.os.kernel.extraFirmwareVol // empty' ${CONFIG_PATH}`,
93+
link,
94+
);
95+
test.is(configValue.trim(), `${VOLUME_NAME}/_data`, 'config.json should have extraFirmwareVol set');
96+
97+
test.comment('config.json updated, waiting for service to be triggered automatically...');
98+
99+
// Poll for service execution (max 5 attempts, 1 second apart)
100+
// Check if ExecMainExitTimestampMonotonic is greater than our recorded time
101+
const maxAttempts = 5;
102+
let serviceTriggered = false;
103+
const timeBeforeMicros = parseInt(monotonicTimeBefore.trim(), 10);
104+
105+
for (let i = 1; i <= maxAttempts; i++) {
106+
await new Promise(r => setTimeout(r, 1000));
107+
108+
const exitTimestamp = await context
109+
.get()
110+
.worker.executeCommandInHostOS(
111+
`systemctl show os-extra-firmware.service --property=ExecMainExitTimestampMonotonic --value`,
112+
link,
113+
);
114+
115+
const exitTimestampMicros = parseInt(exitTimestamp.trim(), 10) || 0;
116+
test.comment(`Attempt ${i}/${maxAttempts}: service exit timestamp = ${exitTimestampMicros}, threshold = ${timeBeforeMicros}`);
117+
118+
if (exitTimestampMicros > timeBeforeMicros) {
119+
test.comment('Service was triggered automatically by config.json change');
120+
serviceTriggered = true;
121+
break;
122+
}
123+
}
124+
125+
test.is(serviceTriggered, true, 'os-extra-firmware.service should be triggered automatically after config.json change');
126+
127+
// Verify service is active
128+
const serviceStatus = await context
129+
.get()
130+
.worker.executeCommandInHostOS(
131+
`systemctl is-active os-extra-firmware.service || true`,
132+
link,
133+
);
134+
test.is(serviceStatus.trim(), 'active', 'os-extra-firmware.service should be active');
135+
};
136+
137+
const verifySysfsPath = async (context, link, test) => {
138+
test.comment('Verifying firmware_class path in sysfs...');
139+
140+
const expectedPath = `/var/lib/docker/volumes/${VOLUME_NAME}/_data`;
141+
142+
// Check if sysfs path exists
143+
const sysfsExists = await context
144+
.get()
145+
.worker.executeCommandInHostOS(
146+
`test -f ${SYSFS_PATH} && echo exists || echo missing`,
147+
link,
148+
);
149+
150+
if (sysfsExists.trim() !== 'exists') {
151+
test.comment(`WARNING: ${SYSFS_PATH} does not exist - kernel may not support runtime firmware path`);
152+
test.pass('Skipping sysfs check - path not available');
153+
return;
154+
}
155+
156+
// Read the current firmware path
157+
const currentPath = await context
158+
.get()
159+
.worker.executeCommandInHostOS(
160+
`cat ${SYSFS_PATH}`,
161+
link,
162+
);
163+
164+
test.is(currentPath.trim(), expectedPath, `Firmware path should be set to ${expectedPath}`);
165+
166+
test.comment(`Firmware path correctly set: ${currentPath.trim()}`);
167+
};
168+
169+
const verifyKernelCmdline = async (context, link, test) => {
170+
test.comment('Verifying firmware_class.path in kernel cmdline...');
171+
172+
const expectedParam = `firmware_class.path=/var/lib/docker/volumes/${VOLUME_NAME}/_data`;
173+
174+
const cmdline = await context
175+
.get()
176+
.worker.executeCommandInHostOS(
177+
`cat /proc/cmdline`,
178+
link,
179+
);
180+
181+
const hasParam = cmdline.includes(expectedParam);
182+
test.is(hasParam, true, `Kernel cmdline should contain ${expectedParam}`);
183+
184+
test.comment(`Kernel cmdline: ${cmdline.trim()}`);
185+
};
186+
187+
const cleanup = async (context, link, test) => {
188+
test.comment('Cleaning up test environment...');
189+
190+
// Restore original config.json
191+
await restoreConfig(context, link, test);
192+
193+
// Remove the test volume
194+
await context
195+
.get()
196+
.worker.executeCommandInHostOS(
197+
`balena volume rm ${VOLUME_NAME} || true`,
198+
link,
199+
);
200+
201+
// Restart service to clear the sysfs path
202+
await context
203+
.get()
204+
.worker.executeCommandInHostOS(
205+
`systemctl restart os-extra-firmware.service || true`,
206+
link,
207+
);
208+
209+
test.comment('Cleanup complete');
210+
};
211+
212+
module.exports = {
213+
title: 'Extra firmware test',
214+
run: async function(test) {
215+
// Backup config.json before making changes
216+
await backupConfig(this.context, this.link, test);
217+
218+
// Step 1: Create a docker volume with test firmware
219+
await createTestVolume(this.context, this.link, test);
220+
221+
// Step 2: Configure config.json and verify service is triggered automatically
222+
await configureExtraFirmwareAndVerifyServiceTriggered(this.context, this.link, test);
223+
224+
// Step 3: Verify the sysfs path is set correctly
225+
await verifySysfsPath(this.context, this.link, test);
226+
227+
// Step 4: Reboot to verify persistence
228+
test.comment('Rebooting devicz...');
229+
await this.context.get().worker.rebootDut(this.link);
230+
test.comment('Device rebooted successfully');
231+
232+
// Step 5: Verify kernel cmdline has the firmware_class.path parameter
233+
await verifyKernelCmdline(this.context, this.link, test);
234+
235+
// Step 6: Verify sysfs path is still set after reboot
236+
await verifySysfsPath(this.context, this.link, test);
237+
238+
// Cleanup and restore config.json
239+
await cleanup(this.context, this.link, test);
240+
},
241+
};
242+

0 commit comments

Comments
 (0)