Skip to content

Commit 6b3e7e4

Browse files
committed
refactor(scenario): move commands to separate files
1 parent 77c7587 commit 6b3e7e4

File tree

7 files changed

+111
-63
lines changed

7 files changed

+111
-63
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ module.exports = {
1313
'@typescript-eslint/explicit-function-return-type': 'off',
1414
'@typescript-eslint/strict-boolean-expressions': 'off',
1515
'@typescript-eslint/restrict-template-expressions': 'off',
16+
'@typescript-eslint/method-signature-style': 'off',
1617
},
1718
};

src/TestScenario.ts

Lines changed: 31 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
11
import chalk from 'chalk';
22
import type { APIClient } from './APIClient';
33
import type { EventManager } from './EventManager';
4-
import type { ExpectEngine } from './ExpectEngine';
5-
import { parseTime } from './utils/parseTime';
64

7-
const validStepKeys = ['name'];
8-
9-
export interface ISetControlParams {
10-
'part-id': string;
11-
control: string;
12-
value: number;
5+
export interface IScenarioCommand {
6+
validate?(params: any): boolean;
7+
run(scenario: TestScenario, client: APIClient, params: any): Promise<void>;
138
}
149

10+
const validStepKeys = ['name'];
11+
1512
export interface IStepDefinition {
1613
name?: string;
17-
'wait-serial': string;
18-
delay: string;
19-
'set-control': ISetControlParams;
14+
[key: string]: any;
2015
}
2116

2217
export interface IScenarioDefinition {
@@ -31,11 +26,13 @@ export class TestScenario {
3126
private stepIndex = 0;
3227
private client?: APIClient;
3328

34-
constructor(
35-
readonly scenario: IScenarioDefinition,
36-
readonly eventManager: EventManager,
37-
readonly expectEngine: ExpectEngine
38-
) {}
29+
readonly handlers: Record<string, IScenarioCommand> = {};
30+
31+
constructor(readonly scenario: IScenarioDefinition, readonly eventManager: EventManager) {}
32+
33+
registerCommands(commands: Record<string, IScenarioCommand>) {
34+
Object.assign(this.handlers, commands);
35+
}
3936

4037
validate() {
4138
const { scenario } = this;
@@ -71,26 +68,24 @@ export class TestScenario {
7168
async start(client: APIClient) {
7269
this.stepIndex = 0;
7370
this.client = client;
74-
await this.nextStep();
75-
}
76-
77-
async nextStep() {
78-
if (this.client?.running) {
79-
void this.client.simPause();
71+
for (const step of this.scenario.steps) {
72+
if (client.running) {
73+
void client.simPause();
74+
}
75+
if (step.name) {
76+
this.log(chalk`{gray Executing step:} {yellow ${step.name}`);
77+
}
78+
await this.executeStep(client, step);
8079
}
80+
this.log(chalk`{green Scenario completed successfully}`);
81+
process.exit(0);
82+
}
8183

82-
const step = this.scenario.steps[this.stepIndex];
83-
if (step == null) {
84-
this.log(chalk`{green Scenario completed successfully}`);
85-
process.exit(0);
86-
}
87-
if (step.name) {
88-
this.log(chalk`{gray Executing step:} {yellow ${step.name}`);
89-
}
90-
for (const key of Object.keys(this.handlers) as Array<keyof typeof this.handlers>) {
84+
async executeStep(client: APIClient, step: IStepDefinition) {
85+
for (const key of Object.keys(this.handlers)) {
9186
if (key in step) {
9287
const value = step[key];
93-
void this.handlers[key](value as any, step);
88+
await this.handlers[key].run(this, client, value);
9489
this.stepIndex++;
9590
return;
9691
}
@@ -103,37 +98,13 @@ export class TestScenario {
10398
console.log(chalk`{cyan [${this.scenario.name}]}`, message);
10499
}
105100

101+
fail(message: string) {
102+
throw new Error(`[${this.client?.lastNanos}ns] ${message}`);
103+
}
104+
106105
async resume() {
107106
await this.client?.simResume(
108107
this.eventManager.timeToNextEvent >= 0 ? this.eventManager.timeToNextEvent : undefined
109108
);
110109
}
111-
112-
handlers = {
113-
'wait-serial': async (text: string) => {
114-
this.expectEngine.expectTexts.push(text);
115-
this.expectEngine.once('match', () => {
116-
this.log(chalk`Expected text matched: {green "${text}"}`);
117-
const textIndex = this.expectEngine.expectTexts.indexOf(text);
118-
if (textIndex >= 0) {
119-
this.expectEngine.expectTexts.splice(textIndex, 1);
120-
}
121-
void this.nextStep();
122-
});
123-
await this.resume();
124-
},
125-
delay: async (value: string, step: IStepDefinition) => {
126-
const nanos = parseTime(value);
127-
const targetNanos = (this.client?.lastNanos ?? 0) + nanos;
128-
this.log(chalk`delay {yellow ${value}}`);
129-
this.eventManager.at(targetNanos, () => {
130-
void this.nextStep();
131-
});
132-
await this.resume();
133-
},
134-
'set-control': async (params: ISetControlParams) => {
135-
await this.client?.controlSet(params['part-id'], params.control, params.value);
136-
await this.nextStep();
137-
},
138-
};
139110
}

src/main.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import { parseConfig } from './config';
1212
import { cliHelp } from './help';
1313
import { loadChips } from './loadChips';
1414
import { readVersion } from './readVersion';
15+
import { DelayCommand } from './scenario/DelayCommand';
16+
import { SetControlCommand } from './scenario/SetControlCommand';
17+
import { WaitSerialCommand } from './scenario/WaitSerialCommand';
1518

1619
const millis = 1_000_000;
1720

@@ -110,9 +113,13 @@ async function main() {
110113
if (resolvedScenarioFile) {
111114
scenario = new TestScenario(
112115
YAML.parse(readFileSync(resolvedScenarioFile, 'utf-8')),
113-
eventManager,
114-
expectEngine
116+
eventManager
115117
);
118+
scenario.registerCommands({
119+
delay: new DelayCommand(eventManager),
120+
'set-control': new SetControlCommand(),
121+
'wait-serial': new WaitSerialCommand(expectEngine),
122+
});
116123
scenario.validate();
117124
}
118125

@@ -166,7 +173,7 @@ async function main() {
166173
console.log('Starting simulation...');
167174
}
168175

169-
await scenario?.start(client);
176+
const scenarioPromise = scenario?.start(client);
170177

171178
if (timeoutNanos) {
172179
eventManager.at(timeoutNanos, () => {
@@ -220,6 +227,8 @@ async function main() {
220227
if (timeToNextEvent > 0) {
221228
await client.simResume(timeToNextEvent);
222229
}
230+
231+
await scenarioPromise;
223232
}
224233

225234
main().catch((err) => {

src/scenario/DelayCommand.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import chalk from 'chalk';
2+
import { type APIClient } from '../APIClient';
3+
import { type EventManager } from '../EventManager';
4+
import { type TestScenario } from '../TestScenario';
5+
import { parseTime } from '../utils/parseTime';
6+
import { promiseAndResolver } from '../utils/promise';
7+
8+
export class DelayCommand {
9+
constructor(readonly eventManager: EventManager) {}
10+
11+
async run(scenario: TestScenario, client: APIClient, value: string) {
12+
const nanos = parseTime(value);
13+
const targetNanos = (client.lastNanos ?? 0) + nanos;
14+
scenario.log(chalk`delay {yellow ${value}}`);
15+
const delayPromise = promiseAndResolver();
16+
this.eventManager.at(targetNanos, () => {
17+
delayPromise.resolve();
18+
});
19+
await Promise.all([scenario.resume(), delayPromise.promise]);
20+
}
21+
}

src/scenario/SetControlCommand.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { type APIClient } from '../APIClient';
2+
import { type TestScenario } from '../TestScenario';
3+
4+
export interface ISetControlParams {
5+
'part-id': string;
6+
control: string;
7+
value: number;
8+
}
9+
10+
export class SetControlCommand {
11+
async run(scenario: TestScenario, client: APIClient, params: ISetControlParams) {
12+
await client.controlSet(params['part-id'], params.control, params.value);
13+
}
14+
}

src/scenario/WaitSerialCommand.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import chalk from 'chalk';
2+
import type { APIClient } from '../APIClient';
3+
import type { ExpectEngine } from '../ExpectEngine';
4+
import type { IScenarioCommand, TestScenario } from '../TestScenario';
5+
import { promiseAndResolver } from '../utils/promise';
6+
7+
export class WaitSerialCommand implements IScenarioCommand {
8+
constructor(readonly expectEngine: ExpectEngine) {}
9+
10+
async run(scenario: TestScenario, client: APIClient, text: string) {
11+
this.expectEngine.expectTexts.push(text);
12+
const { promise, resolve } = promiseAndResolver();
13+
this.expectEngine.once('match', () => {
14+
scenario.log(chalk`Expected text matched: {green "${text}"}`);
15+
const textIndex = this.expectEngine.expectTexts.indexOf(text);
16+
if (textIndex >= 0) {
17+
this.expectEngine.expectTexts.splice(textIndex, 1);
18+
}
19+
resolve();
20+
});
21+
await Promise.all([scenario.resume(), promise]);
22+
}
23+
}

src/utils/promise.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function promiseAndResolver<T = void>() {
2+
let resolve: (value: T | Promise<T>) => void, reject: (reason?: any) => void;
3+
const promise = new Promise<T>((_resolve, _reject) => {
4+
resolve = _resolve;
5+
reject = _reject;
6+
});
7+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
8+
return { promise, resolve: resolve!, reject: reject! };
9+
}

0 commit comments

Comments
 (0)