meross-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/cli/commands/control/execute.js +23 -0
- package/cli/commands/control/index.js +12 -0
- package/cli/commands/control/menu.js +193 -0
- package/cli/commands/control/params/generic.js +229 -0
- package/cli/commands/control/params/index.js +56 -0
- package/cli/commands/control/params/light.js +188 -0
- package/cli/commands/control/params/thermostat.js +166 -0
- package/cli/commands/control/params/timer.js +242 -0
- package/cli/commands/control/params/trigger.js +206 -0
- package/cli/commands/dump.js +35 -0
- package/cli/commands/index.js +34 -0
- package/cli/commands/info.js +221 -0
- package/cli/commands/list.js +112 -0
- package/cli/commands/mqtt.js +187 -0
- package/cli/commands/sniffer/device-sniffer.js +217 -0
- package/cli/commands/sniffer/fake-app.js +233 -0
- package/cli/commands/sniffer/index.js +7 -0
- package/cli/commands/sniffer/message-queue.js +65 -0
- package/cli/commands/sniffer/sniffer-menu.js +676 -0
- package/cli/commands/stats.js +90 -0
- package/cli/commands/status/device-status.js +1403 -0
- package/cli/commands/status/hub-status.js +72 -0
- package/cli/commands/status/index.js +50 -0
- package/cli/commands/status/subdevices/hub-smoke-detector.js +82 -0
- package/cli/commands/status/subdevices/hub-temp-hum-sensor.js +43 -0
- package/cli/commands/status/subdevices/hub-thermostat-valve.js +83 -0
- package/cli/commands/status/subdevices/hub-water-leak-sensor.js +27 -0
- package/cli/commands/status/subdevices/index.js +23 -0
- package/cli/commands/test/index.js +185 -0
- package/cli/config/users.js +108 -0
- package/cli/control-registry.js +875 -0
- package/cli/helpers/client.js +89 -0
- package/cli/helpers/meross.js +106 -0
- package/cli/menu/index.js +10 -0
- package/cli/menu/main.js +648 -0
- package/cli/menu/settings.js +789 -0
- package/cli/meross-cli.js +547 -0
- package/cli/tests/README.md +365 -0
- package/cli/tests/test-alarm.js +144 -0
- package/cli/tests/test-child-lock.js +248 -0
- package/cli/tests/test-config.js +133 -0
- package/cli/tests/test-control.js +189 -0
- package/cli/tests/test-diffuser.js +505 -0
- package/cli/tests/test-dnd.js +246 -0
- package/cli/tests/test-electricity.js +209 -0
- package/cli/tests/test-encryption.js +281 -0
- package/cli/tests/test-garage.js +259 -0
- package/cli/tests/test-helper.js +313 -0
- package/cli/tests/test-hub-mts100.js +355 -0
- package/cli/tests/test-hub-sensors.js +489 -0
- package/cli/tests/test-light.js +253 -0
- package/cli/tests/test-presence.js +497 -0
- package/cli/tests/test-registry.js +419 -0
- package/cli/tests/test-roller-shutter.js +628 -0
- package/cli/tests/test-runner.js +415 -0
- package/cli/tests/test-runtime.js +234 -0
- package/cli/tests/test-screen.js +133 -0
- package/cli/tests/test-sensor-history.js +146 -0
- package/cli/tests/test-smoke-config.js +138 -0
- package/cli/tests/test-spray.js +131 -0
- package/cli/tests/test-temp-unit.js +133 -0
- package/cli/tests/test-template.js +238 -0
- package/cli/tests/test-thermostat.js +919 -0
- package/cli/tests/test-timer.js +372 -0
- package/cli/tests/test-toggle.js +342 -0
- package/cli/tests/test-trigger.js +279 -0
- package/cli/utils/display.js +86 -0
- package/cli/utils/terminal.js +137 -0
- package/package.json +53 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const testRegistry = require('./test-registry');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Test runner for CLI - lightweight custom async execution engine
|
|
9
|
+
* No Mocha dependency - uses simple async/await with structured results
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gets device display name for error messages
|
|
14
|
+
* @param {Object} device - Device instance
|
|
15
|
+
* @returns {string} Device display name
|
|
16
|
+
*/
|
|
17
|
+
function getDeviceName(device) {
|
|
18
|
+
if (!device) return 'Unknown device';
|
|
19
|
+
return device.name || device.uuid || 'Unknown device';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Formats error message with device context
|
|
24
|
+
* @param {Error} error - Error object
|
|
25
|
+
* @param {Object} device - Device instance (optional)
|
|
26
|
+
* @param {string} testName - Test name (optional)
|
|
27
|
+
* @returns {string} Formatted error message
|
|
28
|
+
*/
|
|
29
|
+
function formatError(error, device = null, testName = null) {
|
|
30
|
+
const parts = [];
|
|
31
|
+
|
|
32
|
+
if (testName) {
|
|
33
|
+
parts.push(`Test: ${testName}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (device) {
|
|
37
|
+
parts.push(`Device: ${getDeviceName(device)}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
parts.push(`Error: ${error.message || error}`);
|
|
41
|
+
|
|
42
|
+
if (error.stack) {
|
|
43
|
+
parts.push(`\nStack trace:\n${error.stack}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return parts.join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Runs a single test with timeout and error handling
|
|
51
|
+
* @param {Function} testFn - Test function to execute
|
|
52
|
+
* @param {string} testName - Name of the test
|
|
53
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
54
|
+
* @param {Object} device - Device instance (optional, for context)
|
|
55
|
+
* @returns {Promise<Object>} Test result object
|
|
56
|
+
*/
|
|
57
|
+
async function runSingleTest(testFn, testName, timeout = 30000, device = null) {
|
|
58
|
+
const startTime = Date.now();
|
|
59
|
+
|
|
60
|
+
return new Promise(async (resolve) => {
|
|
61
|
+
const timeoutId = setTimeout(() => {
|
|
62
|
+
resolve({
|
|
63
|
+
name: testName,
|
|
64
|
+
passed: false,
|
|
65
|
+
skipped: false,
|
|
66
|
+
error: `Test timed out after ${timeout}ms`,
|
|
67
|
+
duration: Date.now() - startTime,
|
|
68
|
+
device: device ? getDeviceName(device) : null
|
|
69
|
+
});
|
|
70
|
+
}, timeout);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await testFn();
|
|
74
|
+
|
|
75
|
+
clearTimeout(timeoutId);
|
|
76
|
+
const duration = Date.now() - startTime;
|
|
77
|
+
|
|
78
|
+
// Handle test result - can be boolean, object, or void
|
|
79
|
+
if (result === false) {
|
|
80
|
+
resolve({
|
|
81
|
+
name: testName,
|
|
82
|
+
passed: false,
|
|
83
|
+
skipped: false,
|
|
84
|
+
error: 'Test returned false',
|
|
85
|
+
duration: duration,
|
|
86
|
+
device: device ? getDeviceName(device) : null
|
|
87
|
+
});
|
|
88
|
+
} else if (result && typeof result === 'object') {
|
|
89
|
+
// Test returned structured result
|
|
90
|
+
resolve({
|
|
91
|
+
name: testName,
|
|
92
|
+
passed: result.passed !== false,
|
|
93
|
+
skipped: result.skipped === true,
|
|
94
|
+
error: result.error || null,
|
|
95
|
+
duration: duration,
|
|
96
|
+
device: device ? getDeviceName(device) : null,
|
|
97
|
+
...result
|
|
98
|
+
});
|
|
99
|
+
} else {
|
|
100
|
+
// Test passed (returned true, undefined, or truthy value)
|
|
101
|
+
resolve({
|
|
102
|
+
name: testName,
|
|
103
|
+
passed: true,
|
|
104
|
+
skipped: false,
|
|
105
|
+
error: null,
|
|
106
|
+
duration: duration,
|
|
107
|
+
device: device ? getDeviceName(device) : null
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
clearTimeout(timeoutId);
|
|
112
|
+
resolve({
|
|
113
|
+
name: testName,
|
|
114
|
+
passed: false,
|
|
115
|
+
skipped: false,
|
|
116
|
+
error: formatError(error, device, testName),
|
|
117
|
+
duration: Date.now() - startTime,
|
|
118
|
+
device: device ? getDeviceName(device) : null
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Loads and validates a test file
|
|
126
|
+
* @param {string} testFile - Path to test file
|
|
127
|
+
* @returns {Promise<Object>} Test module with metadata and runTests function
|
|
128
|
+
*/
|
|
129
|
+
async function loadTestFile(testFile) {
|
|
130
|
+
if (!fs.existsSync(testFile)) {
|
|
131
|
+
throw new Error(`Test file not found: ${testFile}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Clear require cache to allow reloading
|
|
135
|
+
delete require.cache[require.resolve(testFile)];
|
|
136
|
+
|
|
137
|
+
const testModule = require(testFile);
|
|
138
|
+
|
|
139
|
+
// For now, allow test files without metadata (they'll use registry metadata)
|
|
140
|
+
// This provides backward compatibility during migration
|
|
141
|
+
// TODO: After migration, require metadata in all test files
|
|
142
|
+
|
|
143
|
+
if (typeof testModule.runTests !== 'function') {
|
|
144
|
+
throw new Error(`Test file ${testFile} must export a 'runTests' function`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return testModule;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Runs tests from a test file
|
|
152
|
+
* @param {string} testType - Test type name
|
|
153
|
+
* @param {Object} context - Test context: { manager, devices, options }
|
|
154
|
+
* @returns {Promise<Object>} Test results
|
|
155
|
+
*/
|
|
156
|
+
async function runTest(testType, context) {
|
|
157
|
+
const { manager, devices = [], options = {} } = context;
|
|
158
|
+
|
|
159
|
+
if (!manager) {
|
|
160
|
+
throw new Error('MerossManager instance is required in context');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Resolve test type (handles aliases)
|
|
164
|
+
const resolvedType = testRegistry.resolveTestType(testType);
|
|
165
|
+
if (!resolvedType) {
|
|
166
|
+
throw new Error(`Unknown test type: ${testType}. Available types: ${getAvailableTestTypes().join(', ')}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Get test file path
|
|
170
|
+
const testFile = getTestFile(resolvedType);
|
|
171
|
+
if (!testFile) {
|
|
172
|
+
throw new Error(`Test file not found for type: ${resolvedType}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const startTime = Date.now();
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
// Load test module
|
|
179
|
+
const testModule = await loadTestFile(testFile);
|
|
180
|
+
const metadata = testModule.metadata || {};
|
|
181
|
+
|
|
182
|
+
// Get metadata from registry if not in test file
|
|
183
|
+
const registryMetadata = testRegistry.getTestMetadata(resolvedType);
|
|
184
|
+
const minDevices = metadata.minDevices || (registryMetadata ? registryMetadata.minDevices : 1);
|
|
185
|
+
const testName = metadata.name || resolvedType;
|
|
186
|
+
const description = metadata.description || (registryMetadata ? registryMetadata.description : 'No description');
|
|
187
|
+
|
|
188
|
+
// Validate devices
|
|
189
|
+
if (devices.length < minDevices) {
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
passed: 0,
|
|
193
|
+
failed: 0,
|
|
194
|
+
skipped: 1,
|
|
195
|
+
duration: Date.now() - startTime,
|
|
196
|
+
error: `Insufficient devices: found ${devices.length}, required ${minDevices}`,
|
|
197
|
+
tests: [{
|
|
198
|
+
name: testName,
|
|
199
|
+
passed: false,
|
|
200
|
+
skipped: true,
|
|
201
|
+
error: `No suitable devices found (found ${devices.length}, need ${minDevices})`,
|
|
202
|
+
duration: 0,
|
|
203
|
+
device: null
|
|
204
|
+
}]
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Run tests
|
|
209
|
+
// Only print initial info if in standalone mode (CLI handles this output)
|
|
210
|
+
if (options.verbose && options.standalone) {
|
|
211
|
+
console.log(`\nRunning ${testName} tests...`);
|
|
212
|
+
console.log(`Description: ${description}`);
|
|
213
|
+
console.log(`Devices: ${devices.length}`);
|
|
214
|
+
devices.forEach((device, idx) => {
|
|
215
|
+
console.log(` [${idx}] ${getDeviceName(device)}`);
|
|
216
|
+
});
|
|
217
|
+
console.log('');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const testResults = await testModule.runTests(context);
|
|
221
|
+
|
|
222
|
+
// Ensure testResults is an array
|
|
223
|
+
const resultsArray = Array.isArray(testResults) ? testResults : [testResults];
|
|
224
|
+
|
|
225
|
+
// Calculate summary
|
|
226
|
+
const passed = resultsArray.filter(r => r.passed && !r.skipped).length;
|
|
227
|
+
const failed = resultsArray.filter(r => !r.passed && !r.skipped).length;
|
|
228
|
+
const skipped = resultsArray.filter(r => r.skipped).length;
|
|
229
|
+
const duration = Date.now() - startTime;
|
|
230
|
+
|
|
231
|
+
// Only print results if verbose mode is enabled (for standalone execution)
|
|
232
|
+
// CLI will handle output formatting, so we skip it here to avoid duplicates
|
|
233
|
+
if (options.verbose && options.standalone) {
|
|
234
|
+
console.log('\n--- Test Results ---');
|
|
235
|
+
resultsArray.forEach((result, idx) => {
|
|
236
|
+
const status = result.skipped ? 'SKIPPED' : (result.passed ? 'PASSED' : 'FAILED');
|
|
237
|
+
const deviceInfo = result.device ? ` [${result.device}]` : '';
|
|
238
|
+
const durationInfo = result.duration ? ` (${result.duration}ms)` : '';
|
|
239
|
+
console.log(`${idx + 1}. ${result.name || 'Unknown test'}${deviceInfo}: ${status}${durationInfo}`);
|
|
240
|
+
|
|
241
|
+
if (result.error) {
|
|
242
|
+
console.log(` Error: ${result.error}`);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
console.log(`\nSummary: ${passed} passed, ${failed} failed, ${skipped} skipped (${duration}ms)`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
success: failed === 0,
|
|
251
|
+
passed: passed,
|
|
252
|
+
failed: failed,
|
|
253
|
+
skipped: skipped,
|
|
254
|
+
duration: duration,
|
|
255
|
+
tests: resultsArray
|
|
256
|
+
};
|
|
257
|
+
} catch (error) {
|
|
258
|
+
const duration = Date.now() - startTime;
|
|
259
|
+
const errorMessage = formatError(error, null, testType);
|
|
260
|
+
|
|
261
|
+
console.error(`\nTest execution error: ${errorMessage}`);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
success: false,
|
|
265
|
+
passed: 0,
|
|
266
|
+
failed: 1,
|
|
267
|
+
skipped: 0,
|
|
268
|
+
duration: duration,
|
|
269
|
+
error: errorMessage,
|
|
270
|
+
tests: [{
|
|
271
|
+
name: testType,
|
|
272
|
+
passed: false,
|
|
273
|
+
skipped: false,
|
|
274
|
+
error: errorMessage,
|
|
275
|
+
duration: duration,
|
|
276
|
+
device: null
|
|
277
|
+
}]
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Test metadata is now managed by test-registry.js
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Gets available test types
|
|
286
|
+
* @returns {Array<string>} Array of test type names
|
|
287
|
+
*/
|
|
288
|
+
function getAvailableTestTypes() {
|
|
289
|
+
return testRegistry.getAvailableTestTypes();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Gets description for a test type
|
|
294
|
+
* @param {string} testType - Test type name or alias
|
|
295
|
+
* @returns {string} Description of the test type
|
|
296
|
+
*/
|
|
297
|
+
function getTestDescription(testType) {
|
|
298
|
+
return testRegistry.getTestDescription(testType);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Gets test file path for a given test type
|
|
303
|
+
* @param {string} testType - Test type name or alias
|
|
304
|
+
* @returns {string|null} Path to test file or null if not found
|
|
305
|
+
*/
|
|
306
|
+
function getTestFile(testType) {
|
|
307
|
+
return testRegistry.getTestFile(testType, __dirname);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Finds devices for a given test type
|
|
312
|
+
* @param {string} testType - Test type name or alias
|
|
313
|
+
* @param {Object} manager - MerossManager instance
|
|
314
|
+
* @returns {Promise<Array>} Array of matching devices
|
|
315
|
+
*/
|
|
316
|
+
async function findDevicesForTestType(testType, manager) {
|
|
317
|
+
const { OnlineStatus } = require('./test-helper');
|
|
318
|
+
const abilities = testRegistry.getRequiredAbilities(testType);
|
|
319
|
+
|
|
320
|
+
if (!abilities || abilities.length === 0) {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const allDevices = manager.getAllDevices();
|
|
325
|
+
if (!allDevices || allDevices.length === 0) {
|
|
326
|
+
const { waitForDevices } = require('./test-helper');
|
|
327
|
+
const deviceList = await waitForDevices(manager, 1000);
|
|
328
|
+
return deviceList.map(({ device }) => device).filter(d => d);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const matchingDevices = [];
|
|
332
|
+
const seenUuids = new Set();
|
|
333
|
+
|
|
334
|
+
// Import MerossHubDevice for filtering (lazy import to avoid circular dependency)
|
|
335
|
+
let MerossHubDevice = null;
|
|
336
|
+
try {
|
|
337
|
+
MerossHubDevice = require('meross-iot').MerossHubDevice;
|
|
338
|
+
} catch (e) {
|
|
339
|
+
// Fallback if import fails
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for (const device of allDevices) {
|
|
343
|
+
const uuid = device.dev?.uuid || device.uuid;
|
|
344
|
+
if (seenUuids.has(uuid)) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (device.onlineStatus !== OnlineStatus.ONLINE &&
|
|
349
|
+
device.dev?.onlineStatus !== OnlineStatus.ONLINE) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Filter out subdevices when searching for hub abilities
|
|
354
|
+
// Subdevices inherit hub abilities but we want the actual hub device
|
|
355
|
+
const isSubdevice = device.constructor.name === 'MerossSubDevice' ||
|
|
356
|
+
device.constructor.name === 'HubTempHumSensor' ||
|
|
357
|
+
device.constructor.name === 'HubWaterLeakSensor' ||
|
|
358
|
+
device.constructor.name === 'HubSmokeDetector' ||
|
|
359
|
+
device.constructor.name === 'HubThermostatValve' ||
|
|
360
|
+
(device.subdeviceId || device._subdeviceId) ||
|
|
361
|
+
(device.hub || device._hub);
|
|
362
|
+
|
|
363
|
+
// Check if this is a hub ability search - if so, exclude subdevices
|
|
364
|
+
const isHubAbilitySearch = abilities.some(ability =>
|
|
365
|
+
ability.startsWith('Appliance.Hub.')
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
if (isHubAbilitySearch && isSubdevice) {
|
|
369
|
+
// Skip subdevices when searching for hub abilities
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const hasAbility = abilities.some(ability =>
|
|
374
|
+
device._abilities && device._abilities[ability]
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
if (hasAbility) {
|
|
378
|
+
seenUuids.add(uuid);
|
|
379
|
+
matchingDevices.push(device);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// For garage, also try by device type if no matches found
|
|
384
|
+
if (testType.toLowerCase() === 'garage' && matchingDevices.length === 0) {
|
|
385
|
+
for (const device of allDevices) {
|
|
386
|
+
const uuid = device.dev?.uuid || device.uuid;
|
|
387
|
+
if (seenUuids.has(uuid)) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const baseDeviceType = device.dev?.deviceType;
|
|
392
|
+
const subdeviceType = device.type || device._type;
|
|
393
|
+
const matchesType = baseDeviceType === 'msg100' || subdeviceType === 'msg100';
|
|
394
|
+
|
|
395
|
+
if (matchesType && (device.onlineStatus === OnlineStatus.ONLINE ||
|
|
396
|
+
device.dev?.onlineStatus === OnlineStatus.ONLINE)) {
|
|
397
|
+
seenUuids.add(uuid);
|
|
398
|
+
matchingDevices.push(device);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return matchingDevices;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
module.exports = {
|
|
407
|
+
runTest,
|
|
408
|
+
runSingleTest,
|
|
409
|
+
getAvailableTestTypes,
|
|
410
|
+
getTestDescription,
|
|
411
|
+
getTestFile,
|
|
412
|
+
findDevicesForTestType,
|
|
413
|
+
getDeviceName,
|
|
414
|
+
formatError
|
|
415
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runtime Device Tests
|
|
5
|
+
* Tests device runtime and system runtime information
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { waitForDeviceConnection, getDeviceName, OnlineStatus } = require('./test-helper');
|
|
9
|
+
|
|
10
|
+
const metadata = {
|
|
11
|
+
name: 'runtime',
|
|
12
|
+
description: 'Tests device runtime and system runtime information',
|
|
13
|
+
requiredAbilities: ['Appliance.System.Runtime'],
|
|
14
|
+
minDevices: 1
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async function runTests(context) {
|
|
18
|
+
const { manager, devices, options = {} } = context;
|
|
19
|
+
const timeout = options.timeout || 30000;
|
|
20
|
+
const results = [];
|
|
21
|
+
|
|
22
|
+
// If no devices provided, discover them
|
|
23
|
+
let testDevices = devices || [];
|
|
24
|
+
if (testDevices.length === 0) {
|
|
25
|
+
// Runtime is typically available on most devices, so we'll test with any online device
|
|
26
|
+
const allDevices = manager.getAllDevices();
|
|
27
|
+
testDevices = allDevices.filter(device => {
|
|
28
|
+
const status = device.onlineStatus !== undefined ? device.onlineStatus : (device.dev?.onlineStatus);
|
|
29
|
+
if (status !== OnlineStatus.ONLINE) return false;
|
|
30
|
+
// Check if device has runtime ability or if it's a common device type
|
|
31
|
+
return device._abilities && (
|
|
32
|
+
device._abilities['Appliance.System.Runtime'] ||
|
|
33
|
+
// Most devices support runtime, so we'll test with any online device
|
|
34
|
+
true
|
|
35
|
+
);
|
|
36
|
+
}).slice(0, 3); // Test up to 3 devices
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Wait for devices to be connected
|
|
40
|
+
for (const device of testDevices) {
|
|
41
|
+
await waitForDeviceConnection(device, timeout);
|
|
42
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (testDevices.length === 0) {
|
|
46
|
+
results.push({
|
|
47
|
+
name: 'should get runtime information',
|
|
48
|
+
passed: false,
|
|
49
|
+
skipped: true,
|
|
50
|
+
error: 'No device with runtime support has been found to run this test',
|
|
51
|
+
device: null
|
|
52
|
+
});
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Test 1: Get runtime information
|
|
57
|
+
let devicesWithRuntime = 0;
|
|
58
|
+
for (const device of testDevices) {
|
|
59
|
+
const deviceName = getDeviceName(device);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Check if device supports runtime
|
|
63
|
+
if (typeof device.updateRuntimeInfo !== 'function') {
|
|
64
|
+
continue; // Skip this device
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const runtimeInfo = await device.updateRuntimeInfo();
|
|
68
|
+
|
|
69
|
+
if (!runtimeInfo) {
|
|
70
|
+
results.push({
|
|
71
|
+
name: 'should get runtime information',
|
|
72
|
+
passed: false,
|
|
73
|
+
skipped: false,
|
|
74
|
+
error: 'updateRuntimeInfo returned null or undefined',
|
|
75
|
+
device: deviceName
|
|
76
|
+
});
|
|
77
|
+
} else if (typeof runtimeInfo !== 'object') {
|
|
78
|
+
results.push({
|
|
79
|
+
name: 'should get runtime information',
|
|
80
|
+
passed: false,
|
|
81
|
+
skipped: false,
|
|
82
|
+
error: 'Runtime info is not an object',
|
|
83
|
+
device: deviceName
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
devicesWithRuntime++;
|
|
87
|
+
// Only add result for first device to avoid too many results
|
|
88
|
+
if (devicesWithRuntime === 1) {
|
|
89
|
+
results.push({
|
|
90
|
+
name: 'should get runtime information',
|
|
91
|
+
passed: true,
|
|
92
|
+
skipped: false,
|
|
93
|
+
error: null,
|
|
94
|
+
device: deviceName,
|
|
95
|
+
details: { runtimeInfo: runtimeInfo }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Only report error for first device
|
|
101
|
+
if (devicesWithRuntime === 0) {
|
|
102
|
+
results.push({
|
|
103
|
+
name: 'should get runtime information',
|
|
104
|
+
passed: false,
|
|
105
|
+
skipped: false,
|
|
106
|
+
error: error.message,
|
|
107
|
+
device: deviceName
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (devicesWithRuntime === 0) {
|
|
114
|
+
results.push({
|
|
115
|
+
name: 'should get runtime information',
|
|
116
|
+
passed: false,
|
|
117
|
+
skipped: true,
|
|
118
|
+
error: 'No devices support updateRuntimeInfo',
|
|
119
|
+
device: null
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Test 2: Cache runtime information
|
|
124
|
+
const testDevice = testDevices[0];
|
|
125
|
+
const deviceName = getDeviceName(testDevice);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
if (typeof testDevice.updateRuntimeInfo !== 'function') {
|
|
129
|
+
results.push({
|
|
130
|
+
name: 'should cache runtime information',
|
|
131
|
+
passed: false,
|
|
132
|
+
skipped: true,
|
|
133
|
+
error: 'Device does not support runtime info',
|
|
134
|
+
device: deviceName
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
// Update runtime info
|
|
138
|
+
await testDevice.updateRuntimeInfo();
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
140
|
+
|
|
141
|
+
// Get cached runtime info
|
|
142
|
+
const cachedInfo = testDevice.cachedSystemRuntimeInfo;
|
|
143
|
+
|
|
144
|
+
if (!cachedInfo) {
|
|
145
|
+
results.push({
|
|
146
|
+
name: 'should cache runtime information',
|
|
147
|
+
passed: false,
|
|
148
|
+
skipped: false,
|
|
149
|
+
error: 'Cached runtime info is null or undefined',
|
|
150
|
+
device: deviceName
|
|
151
|
+
});
|
|
152
|
+
} else if (typeof cachedInfo !== 'object') {
|
|
153
|
+
results.push({
|
|
154
|
+
name: 'should cache runtime information',
|
|
155
|
+
passed: false,
|
|
156
|
+
skipped: false,
|
|
157
|
+
error: 'Cached runtime info is not an object',
|
|
158
|
+
device: deviceName
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
results.push({
|
|
162
|
+
name: 'should cache runtime information',
|
|
163
|
+
passed: true,
|
|
164
|
+
skipped: false,
|
|
165
|
+
error: null,
|
|
166
|
+
device: deviceName,
|
|
167
|
+
details: { cachedInfo: cachedInfo }
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
results.push({
|
|
173
|
+
name: 'should cache runtime information',
|
|
174
|
+
passed: false,
|
|
175
|
+
skipped: false,
|
|
176
|
+
error: error.message,
|
|
177
|
+
device: deviceName
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Test 3: Update runtime info during refreshState
|
|
182
|
+
try {
|
|
183
|
+
if (typeof testDevice.refreshState !== 'function' || typeof testDevice.updateRuntimeInfo !== 'function') {
|
|
184
|
+
results.push({
|
|
185
|
+
name: 'should update runtime info during refreshState',
|
|
186
|
+
passed: false,
|
|
187
|
+
skipped: true,
|
|
188
|
+
error: 'Device does not support refreshState with runtime',
|
|
189
|
+
device: deviceName
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
// Call refreshState which should also update runtime info
|
|
193
|
+
await testDevice.refreshState();
|
|
194
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
195
|
+
|
|
196
|
+
// Verify runtime info was updated
|
|
197
|
+
const cachedInfo = testDevice.cachedSystemRuntimeInfo;
|
|
198
|
+
|
|
199
|
+
if (!cachedInfo) {
|
|
200
|
+
results.push({
|
|
201
|
+
name: 'should update runtime info during refreshState',
|
|
202
|
+
passed: false,
|
|
203
|
+
skipped: false,
|
|
204
|
+
error: 'Runtime info was not cached after refreshState',
|
|
205
|
+
device: deviceName
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
results.push({
|
|
209
|
+
name: 'should update runtime info during refreshState',
|
|
210
|
+
passed: true,
|
|
211
|
+
skipped: false,
|
|
212
|
+
error: null,
|
|
213
|
+
device: deviceName,
|
|
214
|
+
details: { runtimeInfo: cachedInfo }
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
results.push({
|
|
220
|
+
name: 'should update runtime info during refreshState',
|
|
221
|
+
passed: false,
|
|
222
|
+
skipped: false,
|
|
223
|
+
error: error.message,
|
|
224
|
+
device: deviceName
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return results;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
metadata,
|
|
233
|
+
runTests
|
|
234
|
+
};
|