Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calls test runner directly from host process #62

Merged
merged 19 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"test": "npm run test:types && npm run test:unit",
"test:types": "tsc",
"test:unit": "ava",
"test:update": "ava --update-snapshots",
"postinstall": "husky install"
},
"lint-staged": {
Expand Down
2 changes: 1 addition & 1 deletion src/agent/at-driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { AgentMessage } from './messages.js';
* @param {string} [options.url.pathname]
* @param {number | string} [options.url.port]
* @param {Promise<void>} [options.abortSignal]
* @param {AriaATCIAgent.Log} [options.log]
* @param {AriaATCIHost.Log} [options.log]
* @returns {Promise<ATDriver>}
*/
export async function createATDriver({
Expand Down
19 changes: 9 additions & 10 deletions src/agent/create-test-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,40 @@ import { AgentMessage } from './messages.js';

/**
* @param {object} options
* @param {Promise<void>} options.abortSignal resolves when runner should stop
* @param {{hostname: string, port: number | string, pathname: string}} options.atDriverUrl
* @param {AriaATCIShared.BaseURL} options.baseUrl
* @param {AriaATCIAgent.Log} options.log
* @param {AriaATCIHost.Log} options.log
* @param {Promise<void>} options.abortSignal
* @param {AriaATCIAgent.MockOptions} [options.mock]
* @param {AriaATCIAgent.Browser} [options.webDriverBrowser]
* @param {AriaATCIShared.timesOption} options.timesOption
* @param {{toString: function(): string}} options.webDriverUrl
* @returns {Promise<AriaATCIAgent.TestRunner>}
*/
export async function createRunner(options) {
if (!options.abortSignal) {
throw new Error('createRunner requires abortSignal option.');
}
const { abortSignal, log, timesOption } = options;

if (options.mock) {
return new MockTestRunner({ mock: options.mock, ...options });
}
await new Promise(resolve => setTimeout(resolve, 1000));
const { timesOption } = options;

const [browserDriver, atDriver] = await Promise.all([
createBrowserDriver({
url: options.webDriverUrl,
browser: options.webDriverBrowser,
abortSignal: options.abortSignal,
abortSignal,
timesOption,
}).catch(cause => {
throw new Error('Error initializing browser driver', { cause });
}),
createATDriver({
url: options.atDriverUrl,
abortSignal: options.abortSignal,
log: options.log,
abortSignal,
log,
}).catch(cause => {
throw new Error('Error connecting to at-driver', { cause });
}),
]);
return new DriverTestRunner({ ...options, browserDriver, atDriver, timesOption });
return new DriverTestRunner({ ...options, browserDriver, atDriver });
}
4 changes: 2 additions & 2 deletions src/agent/driver-test-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class DriverTestRunner {
/**
* @param {object} options
* @param {AriaATCIShared.BaseURL} options.baseUrl
* @param {AriaATCIAgent.Log} options.log
* @param {AriaATCIHost.Log} options.log
* @param {BrowserDriver} options.browserDriver
* @param {ATDriver} options.atDriver
* @param {AriaATCIShared.timesOption} options.timesOption
Expand Down Expand Up @@ -289,7 +289,7 @@ export class DriverTestRunner {

_appendBaseUrl(pathname) {
// protocol ends with a ':' and pathname starts with a '/'
const base = `${this.baseUrl.protocol}//${this.baseUrl.hostname}:${this.baseUrl.port}${this.baseUrl.pathname}`;
const base = `${this.baseUrl.protocol}://${this.baseUrl.hostname}:${this.baseUrl.port}${this.baseUrl.pathname}`;
ChrisC marked this conversation as resolved.
Show resolved Hide resolved
const newPath = `${this.baseUrl.pathname ? `${this.baseUrl.pathname}/` : ''}${pathname}`;
return new URL(newPath, base);
}
Expand Down
4 changes: 2 additions & 2 deletions src/agent/mock-test-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class MockTestRunner {
/**
* @param {object} options
* @param {AriaATCIShared.BaseURL} options.baseUrl
* @param {AriaATCIAgent.Log} options.log
* @param {AriaATCIHost.Log} options.log
* @param {AriaATCIAgent.MockOptions} options.mock
*/
constructor({ baseUrl, log, mock: config }) {
Expand Down Expand Up @@ -64,7 +64,7 @@ export class MockTestRunner {
* @param {AriaATCIData.CollectedTest} task
*/
async run(task) {
const base = `${this.baseUrl.protocol}//${this.baseUrl.hostname}:${this.baseUrl.port}${this.baseUrl.pathname}`;
const base = `${this.baseUrl.protocol}://${this.baseUrl.hostname}:${this.baseUrl.port}${this.baseUrl.pathname}`;
await this.openPage(
new URL(
`${this.baseUrl.pathname ? `${this.baseUrl.pathname}/` : ''}${task.target.referencePage}`,
Expand Down
43 changes: 5 additions & 38 deletions src/host/cli-run-plan.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ import { Readable } from 'stream';
import fetch, { Response } from 'node-fetch';

import yargs from 'yargs';
import { pickAgentCliOptions } from '../agent/cli.js';
import { AgentMessage } from '../agent/messages.js';

import { AgentController as Agent } from './agent.js';
import { hostMain } from './main.js';
import { HostMessage, createHostLogger } from './messages.js';
import { plansFrom } from './plan-from.js';
import { HostServer } from './server.js';
import { getTimesOption, timesOptionsConfig } from '../shared/times-option.js';
import { timesOptionsConfig } from '../shared/times-option.js';

export const command = 'run-plan [plan-files..]';

Expand Down Expand Up @@ -170,7 +168,7 @@ async function verboseMiddleware(argv) {

let verbosity;
if (debug) {
verbosity = Object.values(HostMessage);
verbosity = Object.values({ ...HostMessage, ...AgentMessage });
} else if (quiet) {
verbosity = [];
} else {
Expand All @@ -183,6 +181,8 @@ async function verboseMiddleware(argv) {
HostMessage.SERVER_LISTENING,
HostMessage.ADD_SERVER_DIRECTORY,
HostMessage.REMOVE_SERVER_DIRECTORY,
AgentMessage.OPEN_PAGE,
AgentMessage.UNCAUGHT_ERROR,
];
}

Expand All @@ -195,7 +195,6 @@ function mainMiddleware(argv) {
mainLoggerMiddleware(argv);
mainTestPlanMiddleware(argv);
mainServerMiddleware(argv);
mainAgentMiddleware(argv);
mainResultMiddleware(argv);
}

Expand Down Expand Up @@ -228,6 +227,7 @@ function mainLoggerMiddleware(argv) {

const logger = createHostLogger();
argv.log = logger.log;
argv.logger = logger;

logger.emitter.on('message', ({ data: { type }, text }) => {
if (verbosity.includes(type)) {
Expand Down Expand Up @@ -260,39 +260,6 @@ function mainServerMiddleware(argv) {
argv.server = new HostServer({ log, baseUrl: { hostname: argv.referenceHostname } });
}

function mainAgentMiddleware(argv) {
const {
log,
agentProtocol: protocol,
agentDebug,
agentQuiet,
agentVerbose,
agentWebDriverUrl,
agentWebDriverBrowser,
agentAtDriverUrl,
agentMock,
agentMockOpenPage,
} = argv;

const timesOption = getTimesOption(argv);

argv.agent = new Agent({
log,
protocol,
config: pickAgentCliOptions({
debug: agentDebug,
quiet: agentQuiet,
verbose: agentVerbose,
webDriverUrl: agentWebDriverUrl,
webDriverBrowser: agentWebDriverBrowser,
atDriverUrl: agentAtDriverUrl,
mock: agentMock,
mockOpenPage: agentMockOpenPage,
timesOption: timesOption,
}),
});
}

function mainResultMiddleware(argv) {
const { stdout } = argv;

Expand Down
106 changes: 66 additions & 40 deletions src/host/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
* @module host
*/

import { startJob } from '../shared/job.js';
import { EventEmitter } from 'events';
import { agentMockOptions } from '../agent/cli.js';
import { createRunner } from '../agent/create-test-runner.js';

import { HostMessage } from './messages.js';
import {
Expand All @@ -13,6 +15,8 @@ import {
addTestLogToTestPlan,
addTestResultToTestPlan,
} from './plan-object.js';
import { getTimesOption } from '../shared/times-option.js';
import { AGENT_TEMPLATES } from '../agent/messages.js';

/**
* @param {AriaATCIHost.Log} log
Expand All @@ -29,33 +33,37 @@ const logUnsuccessfulHTTP = async (log, response) => {

/**
* @param {object} options
* @param {AriaATCIHost.Log} options.log
* @param {AriaATCIHost.Logger} options.logger
* @param {AsyncIterable<AriaATCIHost.TestPlan>} options.plans
* @param {AriaATCIHost.ReferenceFileServer} options.server
* @param {AriaATCIHost.Agent} options.agent
* @param {AriaATCIAgent.TestRunner} options.runner
* @param {AriaATCIHost.EmitPlanResults} options.emitPlanResults
* @param {string} [options.callbackUrl]
* @param {Record<string, string>} [options.callbackHeader]
* @param {typeof fetch} options.fetch
* @param {boolean} options.agentMock
* @param {'request' | 'skip'} options.agentMockOpenPage
* @param {AriaATCIShared.BaseURL} options.agentWebDriverUrl
* @param {AriaATCIAgent.Browser} options.agentWebDriverBrowser
* @param {AriaATCIShared.BaseURL} options.agentAtDriverUrl
*/
export async function hostMain({
log,
plans,
server,
agent,
emitPlanResults,
callbackUrl,
callbackHeader,
fetch,
}) {
export async function hostMain(options) {
const {
logger,
plans,
server,
emitPlanResults,
callbackUrl,
callbackHeader,
agentMock,
agentMockOpenPage,
agentWebDriverUrl,
agentWebDriverBrowser,
agentAtDriverUrl,
} = options;
const { log } = logger;
log(HostMessage.START);

const hostLogJob = startJob(async function (signal) {
for await (const agentLog of signal.cancelable(agent.logs())) {
log(HostMessage.AGENT_LOG, agentLog);
}
});

await server.ready;
log(HostMessage.SERVER_LISTENING, { url: server.baseUrl });

Expand All @@ -65,8 +73,24 @@ export async function hostMain({
log(HostMessage.ADD_SERVER_DIRECTORY, { url: serverDirectory.baseUrl });
setServerOptionsInTestPlan(plan, { baseUrl: serverDirectory.baseUrl });

log(HostMessage.START_AGENT);
await agent.start({ referenceBaseUrl: serverDirectory.baseUrl });
const timesOption = getTimesOption(options);

const emitter = new EventEmitter();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An event emitter seems like more than we need since we're only looking to resolve a single promise at the first occurrence of a single event. Think we can get away with leaking abortSignal's resolver function and invoking it directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if my latest commits address this! I was able to remove the emitter and just trigger via a promise resolver.

const runner = await createRunner({
log,
abortSignal: new Promise(resolve => {
emitter.on(HostMessage.STOP_RUNNER, () => resolve());
}),
timesOption,
baseUrl: serverDirectory.baseUrl,
mock: agentMockOptions({
mock: agentMock,
mockOpenPage: agentMockOpenPage,
}),
webDriverUrl: agentWebDriverUrl,
webDriverBrowser: agentWebDriverBrowser,
atDriverUrl: agentAtDriverUrl,
});

let lastCallbackRequest = Promise.resolve();

Expand All @@ -82,34 +106,37 @@ export async function hostMain({
body.presentationNumber ?? body.testCsvRow
);
lastCallbackRequest = lastCallbackRequest.then(() =>
fetch(perTestUrl, {
method: 'post',
body: JSON.stringify(body),
headers,
}).then(logUnsuccessfulHTTP.bind(null, log))
options
.fetch(perTestUrl, {
method: 'post',
body: JSON.stringify(body),
headers,
})
.then(logUnsuccessfulHTTP.bind(null, log))
);
};

for (const test of plan.tests) {
log(HostMessage.START_TEST);
const testLogJob = startJob(async function (signal) {
for await (const testLog of signal.cancelable(agent.logs())) {
plan = addLogToTestPlan(plan, testLog);
plan = addTestLogToTestPlan(plan, test);
}
});

const file = plan.files.find(({ name }) => name === test.filepath);
const testSource = JSON.parse(textDecoder.decode(file.bufferData));

const { presentationNumber, testId: testCsvRow } = testSource.info;

const callbackBody = presentationNumber ? { presentationNumber } : { testCsvRow };

log(HostMessage.START_TEST, { id: testSource.info.testId, title: testSource.info.title });
const addLogtoPlan = message => {
if (Object.keys(AGENT_TEMPLATES).includes(message.data.type)) {
plan = addLogToTestPlan(plan, message);
plan = addTestLogToTestPlan(plan, test);
}
};
logger.emitter.on('message', addLogtoPlan);

try {
postCallbackWhenEnabled({ ...callbackBody, status: 'RUNNING' });

const result = await agent.run(testSource);
const result = await runner.run(testSource);

const { capabilities, commands } = result;

Expand All @@ -128,21 +155,20 @@ export async function hostMain({
await lastCallbackRequest;
throw exception;
} finally {
await testLogJob.cancel();
logger.emitter.off('message', addLogtoPlan);
}
}

server.removeFiles(serverDirectory);
log(HostMessage.REMOVE_SERVER_DIRECTORY, { url: serverDirectory.baseUrl });

log(HostMessage.STOP_AGENT);
await lastCallbackRequest;
await agent.stop();

emitter.emit(HostMessage.STOP_RUNNER);

await emitPlanResults(plan);
}

await hostLogJob.cancel();

log(HostMessage.STOP_SERVER);
await server.close();

Expand Down
Loading
Loading