blob: 7269e861bd4bc98ba37409277315b07820561753 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import assert from 'assert'; // not esm
import * as vscode from 'vscode';
import { beforeEach, describe, it } from 'mocha';
import { createSandbox } from 'sinon';
import { Ffx, FfxEventType, FuchsiaDevice } from '../../ffx';
import * as logger from '../../logger';
import { StubbedSpawn } from './utils';
describe('FuchsiaDevice', function () {
describe('#constructor()', function () {
it('creates an instance of FuchsiaDevice from the json returned from ffx target list',
function () {
const data = {
'nodename': 'test-device',
// eslint-disable-next-line @typescript-eslint/naming-convention
'rcs_state': 'Y',
'serial': '<unknown>',
// eslint-disable-next-line @typescript-eslint/naming-convention
'target_type': 'workstation.qemu-x64',
// eslint-disable-next-line @typescript-eslint/naming-convention
'target_state': 'Product',
'addresses': ['fe80::3bee:1d4:e205:777e%brqemu', '172.1.1.1']
};
let device = new FuchsiaDevice(data);
assert.strictEqual(device.nodeName, data['nodename']);
assert.strictEqual(device.rcsState, data['rcs_state']);
assert.strictEqual(device.serial, data['serial']);
assert.strictEqual(device.targetType, data['target_type']);
assert.strictEqual(device.targetState, data['target_state']);
let testAddresses = data['addresses'];
let actualAddresses = device.addresses;
for (let i in testAddresses) {
assert.strictEqual(testAddresses[i], actualAddresses[i]);
}
});
});
});
describe('Ffx', function () {
const sandbox = createSandbox();
const TEST_FFX = '/path/to/ffx';
const TEST_CWD = '/path/to/workspace';
const ANALYTICS_FLAG = ['--config', 'fuchsia.analytics.ffx_invoker=vscode-fuchsia'];
var stubbedSpawn: StubbedSpawn;
this.beforeEach(function () {
stubbedSpawn = new StubbedSpawn(sandbox);
const log = vscode.window.createOutputChannel('tool_finder.test');
logger.initLogger(log);
});
this.afterEach(function () {
sandbox.restore();
});
describe('#constructor', function () {
it('creates an instance of ffx', function () {
new Ffx(TEST_CWD, TEST_FFX);
});
});
describe('#rebootTarget', function () {
it('calls ffx target reboot for the default device successfully', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
let promise = ffx.rebootTarget();
stubbedSpawn.spawnEvent.emit('exit', 0);
stubbedSpawn.spawnEvent.emit('close', 0);
let output = await promise;
assert.strictEqual(output, '');
assert.deepStrictEqual(
stubbedSpawn.spawnStubInfo.getCall(0).args,
[TEST_FFX, [...ANALYTICS_FLAG, 'target', 'reboot'], { cwd: TEST_CWD, detached: true }]);
});
it('calls ffx target reboot for the default device and fails', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
const errorMessage = 'Cannot reboot device';
await assert.rejects(async () => {
let promise = ffx.rebootTarget();
stubbedSpawn.spawnEvent.stderr?.emit('data', errorMessage);
stubbedSpawn.spawnEvent.emit('exit', 1);
stubbedSpawn.spawnEvent.emit('close', 1);
await promise;
}, new Error(`ffx returned with non-zero exit code 1: ${errorMessage}`));
});
it('calls ffx target reboot the specified device', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
let device = new FuchsiaDevice({ 'nodename': 'test-device' });
let promise = ffx.rebootTarget(device);
stubbedSpawn.spawnEvent.emit('exit', 0);
stubbedSpawn.spawnEvent.emit('close', 0);
let output = await promise;
assert.strictEqual(output, '');
assert.deepStrictEqual(stubbedSpawn.spawnStubInfo.getCall(0).args, [
TEST_FFX, [...ANALYTICS_FLAG, '--target', 'test-device', 'target', 'reboot'],
{ cwd: TEST_CWD, detached: true }
]);
});
});
describe('#PoweroffTarget', function () {
it('calls ffx target off for the default device successfully', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
let promise = ffx.poweroffTarget();
stubbedSpawn.spawnEvent.emit('exit', 0);
stubbedSpawn.spawnEvent.emit('close', 0);
let output = await promise;
assert.strictEqual(output, '');
assert.deepStrictEqual(
stubbedSpawn.spawnStubInfo.getCall(0).args,
[TEST_FFX, [...ANALYTICS_FLAG, 'target', 'off'], { cwd: TEST_CWD, detached: true }]);
});
it('calls ffx target off for the default device and fails', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
const errorMessage = 'Cannot power off the device';
await assert.rejects(async () => {
let promise = ffx.poweroffTarget();
stubbedSpawn.spawnEvent.stderr?.emit('data', errorMessage);
stubbedSpawn.spawnEvent.emit('exit', 1);
stubbedSpawn.spawnEvent.emit('close', 1);
await promise;
}, new Error(`ffx returned with non-zero exit code 1: ${errorMessage}`));
});
it('calls ffx target off the specified device', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
let device = new FuchsiaDevice({ 'nodename': 'test-device' });
let promise = ffx.poweroffTarget(device);
stubbedSpawn.spawnEvent.emit('exit', 0);
stubbedSpawn.spawnEvent.emit('close', 0);
let output = await promise;
assert.strictEqual(output, '');
assert.deepStrictEqual(stubbedSpawn.spawnStubInfo.getCall(0).args, [
TEST_FFX, [...ANALYTICS_FLAG, '--target', 'test-device', 'target', 'off'],
{ cwd: TEST_CWD, detached: true }
]);
});
});
describe('#showTarget', function () {
it('calls ffx target show for the default device successfully', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
let promise = ffx.showTarget();
// normal exit, no output.
stubbedSpawn.spawnEvent.emit('exit', 0);
stubbedSpawn.spawnEvent.emit('close', 0);
let output = await promise;
assert.strictEqual(output, '');
assert.deepStrictEqual(
stubbedSpawn.spawnStubInfo.getCall(0).args,
[TEST_FFX, [...ANALYTICS_FLAG, 'target', 'show'], { cwd: TEST_CWD, detached: true }]);
});
it('calls ffx target show for the default device and fails', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
const errorMessage = 'Cannot reboot device';
await assert.rejects(async () => {
let promise = ffx.showTarget();
stubbedSpawn.spawnEvent.stderr?.emit('data', errorMessage);
stubbedSpawn.spawnEvent.emit('exit', 1);
stubbedSpawn.spawnEvent.emit('close', 1);
await promise;
}, new Error(`ffx returned with non-zero exit code 1: ${errorMessage}`));
});
it('calls ffx target show for the specified device', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
let device = new FuchsiaDevice({ 'nodename': 'test-device' });
let promise = ffx.showTarget(device);
stubbedSpawn.spawnEvent.emit('exit', 0);
stubbedSpawn.spawnEvent.emit('close', 0);
let output = await promise;
assert.strictEqual(output, '');
assert.deepStrictEqual(stubbedSpawn.spawnStubInfo.getCall(0).args, [
TEST_FFX, [...ANALYTICS_FLAG, '--target', 'test-device', 'target', 'show'],
{ cwd: TEST_CWD, detached: true }
]);
});
});
describe('#ExportSnapshot', function () {
it('calls ffx target snapshot for the default device successfully', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
let promise = ffx.exportSnapshotToCWD();
// normal exit, no output.
stubbedSpawn.spawnEvent.emit('exit', 0);
stubbedSpawn.spawnEvent.emit('close', 0);
let output = await promise;
assert.strictEqual(output, TEST_CWD);
assert.deepStrictEqual(
stubbedSpawn.spawnStubInfo.getCall(0).args,
[TEST_FFX, [...ANALYTICS_FLAG, 'target', 'snapshot', '-d', TEST_CWD],
{ cwd: TEST_CWD, detached: true }]);
});
it('calls ffx target snapshot for the default device and fails', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
const errorMessage = 'Cannot reboot device';
await assert.rejects(async () => {
let promise = ffx.exportSnapshotToCWD();
stubbedSpawn.spawnEvent.stderr?.emit('data', errorMessage);
stubbedSpawn.spawnEvent.emit('exit', 1);
stubbedSpawn.spawnEvent.emit('close', 1);
await promise;
}, new Error(`ffx returned with non-zero exit code 1: ${errorMessage}`));
});
it('calls ffx target snapshot for the specified device', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
let device = new FuchsiaDevice({ 'nodename': 'test-device' });
let promise = ffx.exportSnapshotToCWD(device);
stubbedSpawn.spawnEvent.emit('exit', 0);
stubbedSpawn.spawnEvent.emit('close', 0);
let output = await promise;
assert.strictEqual(output, TEST_CWD);
assert.deepStrictEqual(stubbedSpawn.spawnStubInfo.getCall(0).args, [
TEST_FFX,
[...ANALYTICS_FLAG, '--target', 'test-device', 'target', 'snapshot', '-d', TEST_CWD],
{ cwd: TEST_CWD, detached: true }
]);
});
});
describe('#defaultTarget', function () {
it('sets the specified device to be the default target', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
let device = new FuchsiaDevice({ 'nodename': 'test-device' });
let promise = ffx.defaultTarget(device);
stubbedSpawn.spawnEvent.emit('exit', 0);
stubbedSpawn.spawnEvent.emit('close', 0);
let output = await promise;
assert.strictEqual(output, '');
assert.deepStrictEqual(stubbedSpawn.spawnStubInfo.getCall(0).args, [
TEST_FFX,
[...ANALYTICS_FLAG, 'target', 'default', 'set', 'test-device'],
{ cwd: TEST_CWD, detached: true }
]);
});
});
describe('#events', function () {
it('Verify set path events', () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
let lastEvent: FfxEventType | undefined;
ffx.onDidChangeConfiguration(event => {
lastEvent = event;
});
ffx.path = TEST_FFX;
assert.strictEqual(lastEvent, FfxEventType.ffxPathSet);
ffx.path = undefined;
assert.strictEqual(lastEvent, FfxEventType.ffxPathReset);
ffx.path = TEST_FFX;
assert.strictEqual(lastEvent, FfxEventType.ffxPathSet);
});
});
describe('#getTargetList', function () {
// targetListData: Targets = 2, DefaultTargetCount = 1, ConnectedTargetCount = 2
const targetListData = [
{
'nodename': 'test-device',
// eslint-disable-next-line @typescript-eslint/naming-convention
'rcs_state': 'Y',
'serial': 'serial-11111',
// eslint-disable-next-line @typescript-eslint/naming-convention
'target_type': 'test-product',
// eslint-disable-next-line @typescript-eslint/naming-convention
'target_state': 'Product',
'addresses': ['fe80::41e7:ace8:59b7:3cb7%en11'],
// eslint-disable-next-line @typescript-eslint/naming-convention
'is_default': false
},
{
'nodename': 'another-device',
// eslint-disable-next-line @typescript-eslint/naming-convention
'rcs_state': 'Y',
'serial': 'serial-222222',
// eslint-disable-next-line @typescript-eslint/naming-convention
'target_type': 'test-product',
// eslint-disable-next-line @typescript-eslint/naming-convention
'target_state': 'Product',
'addresses': ['fe80::1010:1010:1010:1010%en11'],
// eslint-disable-next-line @typescript-eslint/naming-convention
'is_default': true
}
];
// newTargetList: Targets = 3, DefaultTargetCount = 1, ConnectedTargetCount = 2
const newTargetList = [
{
'nodename': 'dev1',
// eslint-disable-next-line @typescript-eslint/naming-convention
'rcs_state': 'Y',
// eslint-disable-next-line @typescript-eslint/naming-convention
'serial': 'na', 'target_type': 'na', 'target_state': 'na', 'addresses': ['127.0.0.1'],
// eslint-disable-next-line @typescript-eslint/naming-convention
'is_default': true
},
{
'nodename': 'dev2',
// eslint-disable-next-line @typescript-eslint/naming-convention
'rcs_state': 'Y',
// eslint-disable-next-line @typescript-eslint/naming-convention
'serial': 'na', 'target_type': 'na', 'target_state': 'na', 'addresses': ['127.0.0.1'],
// eslint-disable-next-line @typescript-eslint/naming-convention
'is_default': false
},
{
'nodename': 'dev3',
// eslint-disable-next-line @typescript-eslint/naming-convention
'rcs_state': 'N',
// eslint-disable-next-line @typescript-eslint/naming-convention
'serial': 'na', 'target_type': 'na', 'target_state': 'na', 'addresses': ['127.0.0.1'],
// eslint-disable-next-line @typescript-eslint/naming-convention
'is_default': false
}
];
// newTargetListError: Targets = 2, DefaultTargetCount = 0, ConnectedTargetCount = 0
const newTargetListError = [
{
'nodename': '<unknown1>',
// eslint-disable-next-line @typescript-eslint/naming-convention
'rcs_state': 'N',
// eslint-disable-next-line @typescript-eslint/naming-convention
'serial': 'na', 'target_type': 'na', 'target_state': 'na', 'addresses': ['127.0.0.1'],
// eslint-disable-next-line @typescript-eslint/naming-convention
'is_default': false
},
{
'nodename': '<unknown2>',
// eslint-disable-next-line @typescript-eslint/naming-convention
'rcs_state': 'N',
// eslint-disable-next-line @typescript-eslint/naming-convention
'serial': 'na', 'target_type': 'na', 'target_state': 'na', 'addresses': ['127.0.0.1'],
// eslint-disable-next-line @typescript-eslint/naming-convention
'is_default': false
}
];
/**
* Get first default target name
* @param targetList
* @returns target name or undefine
*/
function defaultTarget(targetList: { [key: string]: FuchsiaDevice; }) {
return Object.values(targetList).map(e => e.isDefault ? e.nodeName : undefined)
.reduce<string | undefined>((acc, cur) => acc ?? cur, undefined);
}
/**
* Returns the number of devices that are set to default, should be 0 or 1.
* @param targetList
* @returns default device count
*/
function defaultTargetCount(targetList: { [key: string]: FuchsiaDevice; }) {
return Object.values(targetList).map(e => e.isDefault ? 1 : 0)
.reduce<number>((acc, cur) => acc + cur, 0);
}
/**
* Return the number of devices that we can connect to
* @param targetList
* @returns number of available devices
*/
function connectedTargetCount(targetList: { [key: string]: FuchsiaDevice; }) {
return Object.values(targetList).map(e => e.rcsState === 'Y' ? 1 : 0)
.reduce<number>((acc, cur) => acc + cur, 0);
}
const TARGET_LIST_ARGS = ['--machine', 'json', 'target', 'list'];
it('gets a map of device name to FuchsiaDevice.', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
stubbedSpawn.spawnStubInfo.callsFake(function () {
setTimeout(() => {
stubbedSpawn.spawnEvent.stdout?.emit('data', JSON.stringify(targetListData));
stubbedSpawn.spawnEvent.emit('exit', 0);
stubbedSpawn.spawnEvent.emit('close', 0);
}, 1);
return stubbedSpawn.spawnEvent;
});
let deviceList = await ffx.getTargetList();
assert.deepStrictEqual(
stubbedSpawn.spawnStubInfo.getCall(0).args,
[TEST_FFX, [...ANALYTICS_FLAG, ...TARGET_LIST_ARGS], { cwd: TEST_CWD, detached: true }]);
// targetListData: Targets = 2, DefaultTargetCount = 1, ConnectedTargetCount = 2
assert.strictEqual(Object.keys(deviceList).length, 2);
assert.strictEqual(defaultTargetCount(deviceList), 1);
assert.strictEqual(connectedTargetCount(deviceList), 2);
assert.strictEqual(defaultTarget(deviceList), 'another-device');
});
it('Timeout while calling FFX Target List', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
stubbedSpawn.spawnStubInfo.callsFake(function () {
return stubbedSpawn.spawnEvent;
});
await vscode.workspace.getConfiguration('fuchsia').update('connectionTimeout', 10);
try {
await ffx.getTargetList();
} catch (e) {
assert.deepStrictEqual(true, e instanceof Error);
if (e instanceof Error) {
assert.strictEqual(true, e.toString().includes('timeout'), `"timeout" is not in "${e}"`);
return;
}
}
throw Error('Target list does not timeout!');
});
/**
* Send two FFX target lists sequentially and get the final IDE device list
* @param testDataOne first target list returned by FFX target list
* @param testDataTwo second target list returned by FFX target list
* @returns a promise to the final IDE device list
*/
async function setupTwoDeviceListTest(testDataOne: string, testDataTwo: string) {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
let useTestDataTwo = false;
stubbedSpawn.spawnStubInfo.callsFake(function () {
setTimeout(() => {
let data = useTestDataTwo ? testDataTwo : testDataOne;
stubbedSpawn.spawnEvent.stdout?.emit('data', data);
stubbedSpawn.spawnEvent.emit('exit', 0);
stubbedSpawn.spawnEvent.emit('close', 0);
}, 1);
return stubbedSpawn.spawnEvent;
});
let deviceList = await ffx.getTargetList();
assert.deepStrictEqual(
stubbedSpawn.spawnStubInfo.getCall(0).args,
[TEST_FFX, [...ANALYTICS_FLAG, ...TARGET_LIST_ARGS], { cwd: TEST_CWD, detached: true }]);
// check number of devices
assert.strictEqual(Object.keys(deviceList).length, targetListData.length);
//Use test data 2 on second call
useTestDataTwo = true;
deviceList = await ffx.getTargetList();
assert.deepStrictEqual(
stubbedSpawn.spawnStubInfo.getCall(1).args,
[TEST_FFX, [...ANALYTICS_FLAG, ...TARGET_LIST_ARGS], { cwd: TEST_CWD, detached: true }]);
return deviceList;
}
it('Verify device list after targetListData and then empty', async () => {
let deviceList = await setupTwoDeviceListTest(JSON.stringify(targetListData), '');
// Empty device list
assert.strictEqual(Object.keys(deviceList).length, 0);
});
it('Verify device list after targetListData and then newTargetList', async () => {
let deviceList = await setupTwoDeviceListTest(
JSON.stringify(targetListData),
JSON.stringify(newTargetList)
);
// newTargetList: Targets = 3, DefaultTargetCount = 1, ConnectedTargetCount = 2
assert.strictEqual(Object.keys(deviceList).length, 3);
assert.strictEqual(defaultTargetCount(deviceList), 1);
assert.strictEqual(connectedTargetCount(deviceList), 2);
assert.strictEqual(defaultTarget(deviceList), 'dev1');
});
it('Verify device list after targetListData and then newTargetListError', async () => {
let deviceList = await setupTwoDeviceListTest(
JSON.stringify(targetListData),
JSON.stringify(newTargetListError)
);
// newTargetListError: Targets = 2, DefaultTargetCount = 0, ConnectedTargetCount = 0
assert.strictEqual(Object.keys(deviceList).length, 2);
assert.strictEqual(defaultTargetCount(deviceList), 0);
assert.strictEqual(connectedTargetCount(deviceList), 0);
assert.strictEqual(defaultTarget(deviceList), undefined);
});
it('gets a map of device name to FuchsiaDevice and gets the default.', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
stubbedSpawn.spawnStubInfo.callsFake(function () {
setTimeout(() => {
stubbedSpawn.spawnEvent.stdout?.emit('data', JSON.stringify(targetListData));
stubbedSpawn.spawnEvent.emit('exit', 0);
stubbedSpawn.spawnEvent.emit('close', 0);
}, 1);
return stubbedSpawn.spawnEvent;
});
let deviceList = await ffx.getTargetList();
assert.deepStrictEqual(
stubbedSpawn.spawnStubInfo.getCall(0).args,
[TEST_FFX, [...ANALYTICS_FLAG, ...TARGET_LIST_ARGS], { cwd: TEST_CWD, detached: true }]);
// check number of devices
assert.strictEqual(Object.keys(deviceList).length, targetListData.length);
// check there is no default.
for (let name in deviceList) {
assert.strictEqual(
deviceList[name].isDefault, deviceList[name].nodeName === 'another-device');
}
});
it('Verify set target event', async () => {
const ffx = new Ffx(TEST_CWD, TEST_FFX);
stubbedSpawn.spawnStubInfo.callsFake(function () {
setTimeout(() => {
stubbedSpawn.spawnEvent.stdout?.emit('data', JSON.stringify(targetListData));
stubbedSpawn.spawnEvent.emit('exit', 0);
stubbedSpawn.spawnEvent.emit('close', 0);
}, 1);
return stubbedSpawn.spawnEvent;
});
let count = 0;
ffx.onSetTarget(target => {
count += 1;
});
assert.strictEqual(count, 0);
ffx.path = TEST_FFX;
await new Promise(resolve => setTimeout(resolve, 50));
assert.strictEqual(count, 1);
});
});
describe('#runLog', () => {
let ffx: Ffx;
const LOG_ARGS = ['--target', 'foo', '--machine', 'json', 'log'];
beforeEach(() => {
ffx = new Ffx(TEST_CWD, TEST_FFX);
});
it('parses stdout and sends it to the callback', () => {
let received;
const TEST_DATA: Object = { 'foo': 'bar' };
stubbedSpawn.spawnStubInfo.callsFake(() => stubbedSpawn.spawnEvent);
ffx.runLog('foo', [], (data) => {
received = data;
});
assert.deepStrictEqual(
stubbedSpawn.spawnStubInfo.getCall(0).args,
[TEST_FFX, [...ANALYTICS_FLAG, ...LOG_ARGS], { cwd: TEST_CWD, detached: true }]);
stubbedSpawn.spawnEvent.stdout?.emit('data', JSON.stringify(TEST_DATA));
assert.deepStrictEqual(received, TEST_DATA);
});
it('sends stderr to the log', () => {
stubbedSpawn.spawnStubInfo.callsFake(() => stubbedSpawn.spawnEvent);
// spy on the singleton instead of the module method, since we can't
// modify singletons and such with ESM
let spiedWarn = sandbox.spy(logger.logger, 'warn');
ffx.runLog('foo', [], (_) => { });
stubbedSpawn.spawnEvent.stderr?.emit('data', 'oh no');
assert(spiedWarn.calledOnce);
assert(
spiedWarn.getCall(0).args[0].endsWith('oh no'));
});
});
});