Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion src/BrightScriptCommands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Module.prototype.require = function hijacked(file) {
import { BrightScriptCommands } from './BrightScriptCommands';
import { util } from './util';
import { rokuDeploy } from 'roku-deploy';
import { vscodeContextManager } from './managers/VscodeContextManager';

describe('BrightScriptFileUtils ', () => {
let commands: BrightScriptCommands;
Expand Down Expand Up @@ -292,7 +293,7 @@ describe('BrightScriptFileUtils ', () => {
};
//password resolution is delegated to UserInputManager (tested in its own spec)
userInputManager = {
promptForHost: sandbox.stub().resolves('1.2.3.4'),
promptForHost: sandbox.stub().resolves({ host: '1.2.3.4', deviceInfo: undefined }),
resolveDevicePassword: sandbox.stub().resolves({ status: 'ok', password: 'pw' })
};
//ctor: remoteControlManager, whatsNewManager, context, deviceManager, userInputManager, localPackageManager, credentialStore
Expand Down Expand Up @@ -443,6 +444,49 @@ describe('BrightScriptFileUtils ', () => {
});
});

describe('getHealthyActiveHost', () => {
let sandbox: sinon.SinonSandbox;
let localCommands: BrightScriptCommands;
let deviceManager: any;

beforeEach(() => {
sandbox = sinon.createSandbox();
deviceManager = {
healthCheckDevice: sandbox.stub().resolves(true),
getDevice: sandbox.stub().returns({ ip: '1.2.3.4', deviceInfo: { 'serial-number': 'SN123' } })
};
localCommands = new BrightScriptCommands({} as any, {} as any, vscode.context, deviceManager, {} as any, {} as any, {} as any);
sandbox.stub(vscodeContextManager, 'get').returns('1.2.3.4');
});

afterEach(() => {
sandbox.restore();
});

it('returns the host with its device info when the active host is healthy', async () => {
const result = await localCommands.getHealthyActiveHost();
assert.deepEqual(result, { host: '1.2.3.4', deviceInfo: { 'serial-number': 'SN123' } });
});

it('returns undefined when no active host is set', async () => {
(vscodeContextManager.get as sinon.SinonStub).returns(undefined);
const result = await localCommands.getHealthyActiveHost();
assert.isUndefined(result);
});

it('returns undefined when the active host fails the health check', async () => {
deviceManager.healthCheckDevice.resolves(false);
const result = await localCommands.getHealthyActiveHost();
assert.isUndefined(result);
});

it('returns undefined when no device info could be read back', async () => {
deviceManager.getDevice.returns(undefined);
const result = await localCommands.getHealthyActiveHost();
assert.isUndefined(result);
});
});

describe('onToggleXml ', () => {
it('does nothing when no active document', () => {
vscode.window.activeTextEditor = undefined;
Expand Down
26 changes: 18 additions & 8 deletions src/BrightScriptCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { util } from './util';
import { util as rokuDebugUtil } from 'roku-debug/dist/util';
import type { RemoteControlManager, RemoteControlModeInitiator } from './managers/RemoteControlManager';
import type { WhatsNewManager } from './managers/WhatsNewManager';
import type { ConfiguredDevice, DeviceManager, RokuDevice } from './deviceDiscovery/DeviceManager';
import type { ConfiguredDevice, DeviceManager, HostWithDeviceInfo, RokuDevice } from './deviceDiscovery/DeviceManager';
import * as xml2js from 'xml2js';
import { firstBy } from 'thenby';
import type { UserInputManager } from './managers/UserInputManager';
Expand Down Expand Up @@ -387,7 +387,7 @@ export class BrightScriptCommands {

this.registerCommand('openRegistryInBrowser', async (host: string) => {
if (!host) {
host = await this.userInputManager.promptForHost();
host = (await this.userInputManager.promptForHost())?.host;
}

let responseText = await util.spinAsync('Fetching app list', async () => {
Expand Down Expand Up @@ -439,7 +439,7 @@ export class BrightScriptCommands {
ip = deviceOrItem;
}
if (!ip) {
ip = await this.userInputManager.promptForHost();
ip = (await this.userInputManager.promptForHost())?.host;
}
if (!ip) {
throw new Error('Tried to set active device but failed.');
Expand Down Expand Up @@ -855,7 +855,7 @@ export class BrightScriptCommands {
private async resolveDeviceHost(host?: string): Promise<{ host: string; serialNumber: string | undefined; label: string } | undefined> {
if (!host) {
try {
host = await this.userInputManager.promptForHost();
host = (await this.userInputManager.promptForHost())?.host;
} catch {
// promptForHost rejects when the user dismisses the picker; treat as a cancel.
return undefined;
Expand Down Expand Up @@ -929,7 +929,7 @@ export class BrightScriptCommands {
this.host = config.get('host');
// eslint-disable-next-line no-template-curly-in-string
if ((!this.host || this.host === '${promptForHost}') && showPrompt) {
this.host = await this.userInputManager.promptForHost();
this.host = (await this.userInputManager.promptForHost())?.host;
}
}
if (!this.host) {
Expand Down Expand Up @@ -1086,15 +1086,25 @@ export class BrightScriptCommands {
}

/**
* Return the active host IP if one is set and passes a health check; otherwise undefined.
* Return the active host (paired with its raw device-info) if one is set and passes a health
* check; otherwise undefined. The health check refreshes the device in the device manager, so
* the device-info is read back from there without an extra request.
*/
public async getHealthyActiveHost(): Promise<string | undefined> {
public async getHealthyActiveHost(): Promise<HostWithDeviceInfo | undefined> {
const activeHost = vscodeContextManager.get<string>('activeHost');
if (!activeHost) {
return undefined;
}
const isHealthy = await this.deviceManager.healthCheckDevice({ ip: activeHost }, true, false);
return isHealthy ? activeHost : undefined;
if (!isHealthy) {
return undefined;
}
const deviceInfo = this.deviceManager.getDevice({ ip: activeHost })?.deviceInfo;
//deviceInfo is required on HostWithDeviceInfo, so if we couldn't read it back, report no healthy active host
if (!deviceInfo) {
return undefined;
}
return { host: activeHost, deviceInfo: deviceInfo };
}

/**
Expand Down
130 changes: 127 additions & 3 deletions src/DebugConfigurationProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@
// Override any properties that would cause a prompt if not overridden
configDefaults.host = '192.168.1.100';
configDefaults.password = 'aaaa';
//return an empty deviceInfo response
sinon.stub(rokuDeploy, 'getDeviceInfo').returns(Promise.reject(new Error('Failure during test')));
//probing the host resolves a reachable developer device
sinon.stub(rokuDeploy, 'getDeviceInfo').resolves({ 'developer-enabled': 'true', 'serial-number': 'SN-TEST' } as any);
// short-circuit the password candidate validation loop so tests can focus on config resolution logic
sinon.stub(DeviceManager.prototype, 'validateDevicePassword').resolves('ok');
});
Expand Down Expand Up @@ -156,6 +156,124 @@
expect(config.password).to.equal('pass1234');
});

describe('device info', () => {
it('attaches the raw device info from the probed device to the resolved config without a separate network request', async () => {
sinon.stub(configProvider, 'getBsConfig').returns({});
const deviceInfo = {
'serial-number': 'abc123',
'developer-enabled': 'true',
'software-version': '11.5.0'
};
sinon.stub(deviceManager, 'validateAndAddDevice').resolves({ ip: '1.2.3.4', deviceInfo: deviceInfo } as any);

const config = await configProvider.resolveDebugConfiguration(folder, <any>{
host: '1.2.3.4',
type: 'brightscript',
password: 'aaaa'
});

//the raw device info should be passed straight through to the launch config
expect(config.deviceInfo).to.eql(deviceInfo);

Check failure on line 176 in src/DebugConfigurationProvider.spec.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest)

Property 'deviceInfo' does not exist on type 'BrightScriptLaunchConfiguration'.

Check failure on line 176 in src/DebugConfigurationProvider.spec.ts

View workflow job for this annotation

GitHub Actions / ci (macos-latest)

Property 'deviceInfo' does not exist on type 'BrightScriptLaunchConfiguration'.
//device info is reused from the probe, so no extra getDeviceInfo network request should happen
expect((rokuDeploy.getDeviceInfo as any).called).to.be.false;
});

it('throws when the probed device reports developer mode disabled', async () => {
sinon.stub(configProvider, 'getBsConfig').returns({});
sinon.stub(deviceManager, 'validateAndAddDevice').resolves({
ip: '1.2.3.4',
deviceInfo: { 'developer-enabled': 'false' }
} as any);

try {
await configProvider.resolveDebugConfiguration(folder, <any>{
host: '1.2.3.4',
type: 'brightscript',
password: 'aaaa'
});
assert.fail('Should have thrown exception');
} catch (e) {
expect((e as Error)?.message).to.contain('developer mode is disabled');
}
});

it('throws when the probed device returns no device info', async () => {
sinon.stub(configProvider, 'getBsConfig').returns({});
sinon.stub(deviceManager, 'validateAndAddDevice').resolves({ ip: '1.2.3.4', deviceInfo: {} } as any);

try {
await configProvider.resolveDebugConfiguration(folder, <any>{
host: '1.2.3.4',
type: 'brightscript',
password: 'aaaa'
});
assert.fail('Should have thrown exception');
} catch (e) {
expect((e as Error)?.message).to.contain('unable to reach device');
}
});
});

describe('processHostParameter', () => {
it('reuses the device from the picker instead of probing again', async () => {
const deviceInfo = { 'serial-number': 'abc123', 'developer-enabled': 'true' };
const device = { ip: '1.2.3.4', serialNumber: 'abc123', deviceInfo: deviceInfo } as any;
//the picker resolved the host, so the device is already registered
(configProvider as any).brightScriptCommands = { getHealthyActiveHost: sinon.stub().resolves(undefined) };
sinon.stub(userInputManager, 'promptForHost').resolves({ host: '1.2.3.4', deviceInfo: deviceInfo });
const getDeviceStub = sinon.stub(deviceManager, 'getDevice').returns(device);
const validateStub = sinon.stub(deviceManager, 'validateAndAddDevice').resolves(undefined);

const result = await (configProvider as any).processHostParameter({ host: '' });

expect(getDeviceStub.calledWith({ ip: '1.2.3.4' })).to.be.true;
expect(validateStub.called).to.be.false;
expect(result.deviceInfo).to.eql(deviceInfo);
});

it('reuses the device from the healthy active host instead of probing again', async () => {
const deviceInfo = { 'serial-number': 'abc123', 'developer-enabled': 'true' };
const device = { ip: '1.2.3.4', serialNumber: 'abc123', deviceInfo: deviceInfo } as any;
//the active-host lookup resolved the host, so the device is already registered
(configProvider as any).brightScriptCommands = { getHealthyActiveHost: sinon.stub().resolves({ host: '1.2.3.4', deviceInfo: deviceInfo }) };
const promptStub = sinon.stub(userInputManager, 'promptForHost');
const getDeviceStub = sinon.stub(deviceManager, 'getDevice').returns(device);
const validateStub = sinon.stub(deviceManager, 'validateAndAddDevice').resolves(undefined);

const result = await (configProvider as any).processHostParameter({ host: '' });

expect(promptStub.called).to.be.false;
expect(getDeviceStub.calledWith({ ip: '1.2.3.4' })).to.be.true;
expect(validateStub.called).to.be.false;
expect(result.deviceInfo).to.eql(deviceInfo);
});

it('probes the host and attaches its device info when not chosen through the picker', async () => {
const deviceInfo = { 'serial-number': 'abc123' };
const device = { ip: '1.2.3.4', serialNumber: 'abc123', deviceInfo: deviceInfo } as any;
const getDeviceStub = sinon.stub(deviceManager, 'getDevice');
const validateStub = sinon.stub(deviceManager, 'validateAndAddDevice').resolves(device);

const result = await (configProvider as any).processHostParameter({ host: '1.2.3.4' });

expect(validateStub.calledWith('1.2.3.4')).to.be.true;
expect(getDeviceStub.called).to.be.false;
expect(result.deviceInfo).to.eql(deviceInfo);
});

it('throws when the probe yields no device info', async () => {
sinon.stub(deviceManager, 'validateAndAddDevice').resolves({ ip: '1.2.3.4', deviceInfo: {} } as any);

let threw: Error | undefined;
try {
await (configProvider as any).processHostParameter({ host: '1.2.3.4' });
} catch (e) {
threw = e as Error;
}
expect(threw?.message).to.contain('unable to reach device');
});
});

it('uses the default values if not provided', async () => {
const config = await configProvider.resolveDebugConfiguration(folder, <any>{ type: 'brightscript' });
const configDefaults = (configProvider as any).configDefaults;
Expand Down Expand Up @@ -424,7 +542,13 @@
config: Partial<BrightScriptLaunchConfiguration>,
result: Partial<BrightScriptLaunchConfiguration>,
device: any
) => (configProvider as any).processPasswordParameter(config, result, device);
) => {
//processPasswordParameter now reads the serial number from result.deviceInfo instead of a device arg
if (device?.serialNumber) {
(result as any).deviceInfo = { 'serial-number': device.serialNumber };
}
return (configProvider as any).processPasswordParameter(config, result);
};

/**
* Register the given serial number in `brightscript.devices[]` user settings
Expand Down
Loading
Loading