@dionlarson/playwright-orchestrator-file 1.3.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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Playwright Orchestrator local file plugin
2
+
3
+ For more details visit @[playwright-orchestrator/core](https://www.npmjs.com/package/@playwright-orchestrator/core) page
@@ -0,0 +1,3 @@
1
+ export interface CreateArgs {
2
+ directory: string;
3
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { TestItem, Adapter, TestRunConfig, TestRunReport, ResultTestParams, SaveTestRunParams } from '@playwright-orchestrator/core';
2
+ import { CreateArgs } from './create-args.js';
3
+ export declare class FileAdapter extends Adapter {
4
+ private readonly dir;
5
+ constructor(createArgs: CreateArgs);
6
+ startShard(runId: string): Promise<TestRunConfig>;
7
+ finishShard(runId: string): Promise<void>;
8
+ getNextTest(runId: string, config: TestRunConfig): Promise<TestItem | undefined>;
9
+ failTest(params: ResultTestParams): Promise<void>;
10
+ finishTest(params: ResultTestParams): Promise<void>;
11
+ saveTestRun({ runId, testRun, args, historyWindow }: SaveTestRunParams): Promise<void>;
12
+ initialize(): Promise<void>;
13
+ dispose(): Promise<void>;
14
+ getReportData(runId: string): Promise<TestRunReport>;
15
+ private loadTestInfos;
16
+ private addResult;
17
+ private updateHistory;
18
+ private getRunIdFilePath;
19
+ private getRunConfigPath;
20
+ private getHistoryRunPath;
21
+ private getResultsRunPath;
22
+ }
@@ -0,0 +1,176 @@
1
+ import { Adapter, RunStatus, TestStatus, } from '@playwright-orchestrator/core';
2
+ import { lock } from 'proper-lockfile';
3
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
4
+ import { existsSync } from 'node:fs';
5
+ export class FileAdapter extends Adapter {
6
+ dir;
7
+ constructor(createArgs) {
8
+ super();
9
+ this.dir = createArgs.directory;
10
+ }
11
+ async startShard(runId) {
12
+ const file = this.getRunConfigPath(runId);
13
+ const release = await lock(file, { retries: 100 });
14
+ const config = JSON.parse(await readFile(file, 'utf-8'));
15
+ if (config.status === RunStatus.Created || config.status === RunStatus.Finished) {
16
+ config.status = config.status === RunStatus.Created ? RunStatus.Run : RunStatus.RepeatRun;
17
+ config.updated = Date.now();
18
+ await writeFile(file, JSON.stringify(config));
19
+ if (config.status === RunStatus.RepeatRun) {
20
+ const results = JSON.parse(await readFile(this.getResultsRunPath(runId), 'utf-8'));
21
+ const failed = results
22
+ .filter((r) => r.status === TestStatus.Failed)
23
+ .map(({ file, testId, order, position, project, timeout }) => ({
24
+ file,
25
+ testId,
26
+ order,
27
+ position,
28
+ project,
29
+ timeout,
30
+ }));
31
+ const rest = results.filter((r) => r.status !== TestStatus.Failed);
32
+ await writeFile(this.getRunIdFilePath(runId), JSON.stringify(failed, null, 2));
33
+ await writeFile(this.getResultsRunPath(runId), JSON.stringify(rest, null, 2), 'utf-8');
34
+ }
35
+ }
36
+ await release();
37
+ return config;
38
+ }
39
+ async finishShard(runId) {
40
+ const file = this.getRunConfigPath(runId);
41
+ const release = await lock(file, { retries: 100 });
42
+ const config = JSON.parse(await readFile(file, 'utf-8'));
43
+ config.status = RunStatus.Finished;
44
+ config.updated = Date.now();
45
+ await writeFile(file, JSON.stringify(config));
46
+ await release();
47
+ }
48
+ async getNextTest(runId, config) {
49
+ const file = this.getRunIdFilePath(runId);
50
+ const release = await lock(file, { retries: 100 });
51
+ const tests = JSON.parse(await readFile(file, 'utf-8'));
52
+ const test = tests.pop();
53
+ await writeFile(file, JSON.stringify(tests, null, 2));
54
+ await release();
55
+ return test;
56
+ }
57
+ async failTest(params) {
58
+ await this.addResult(TestStatus.Failed, params);
59
+ }
60
+ async finishTest(params) {
61
+ await this.addResult(TestStatus.Passed, params);
62
+ }
63
+ async saveTestRun({ runId, testRun, args, historyWindow }) {
64
+ const file = this.getRunConfigPath(runId);
65
+ await mkdir(this.dir, { recursive: true });
66
+ const testConfig = {
67
+ ...testRun.config,
68
+ args: args,
69
+ status: 0,
70
+ updated: Date.now(),
71
+ historyWindow,
72
+ };
73
+ await writeFile(file, JSON.stringify(testConfig, null, 2));
74
+ let tests = this.transformTestRunToItems(testRun.testRun);
75
+ const testInfos = await this.loadTestInfos(tests);
76
+ tests = this.sortTests(tests, testInfos, { historyWindow, reverse: true });
77
+ await writeFile(this.getRunIdFilePath(runId), JSON.stringify(tests, null, 2));
78
+ await writeFile(this.getResultsRunPath(runId), '[]');
79
+ }
80
+ async initialize() {
81
+ return;
82
+ }
83
+ async dispose() { }
84
+ async getReportData(runId) {
85
+ const config = JSON.parse(await readFile(this.getRunConfigPath(runId), 'utf-8'));
86
+ const tests = JSON.parse(await readFile(this.getResultsRunPath(runId), 'utf-8'));
87
+ return {
88
+ runId,
89
+ config,
90
+ tests: tests.map(({ file, status, project, position, report }) => ({
91
+ averageDuration: report.ema,
92
+ duration: report.duration,
93
+ fails: report.fails,
94
+ title: report.title,
95
+ file,
96
+ position,
97
+ project,
98
+ status,
99
+ lastSuccessfulRunTimestamp: report.lastSuccessfulRunTimestamp,
100
+ })),
101
+ };
102
+ }
103
+ async loadTestInfos(test) {
104
+ const history = !existsSync(this.getHistoryRunPath())
105
+ ? {}
106
+ : JSON.parse(await readFile(this.getHistoryRunPath(), 'utf-8'));
107
+ for (const t of test) {
108
+ if (!history[t.testId]) {
109
+ history[t.testId] = {
110
+ ema: 0,
111
+ created: Date.now(),
112
+ history: [],
113
+ };
114
+ }
115
+ }
116
+ await writeFile(this.getHistoryRunPath(), JSON.stringify(history, null, 2));
117
+ return new Map(Object.entries(history).map(([testId, { ema, history }]) => [
118
+ testId,
119
+ { ema, fails: history.filter((h) => h.status === TestStatus.Failed).length },
120
+ ]));
121
+ }
122
+ async addResult(status, params) {
123
+ const { runId, test, testResult } = params;
124
+ const file = this.getResultsRunPath(runId);
125
+ const release = await lock(file, { retries: 100 });
126
+ const results = JSON.parse(await readFile(file, 'utf-8'));
127
+ const testId = this.getTestId({ ...test, ...testResult });
128
+ const stats = await this.updateHistory(status, params);
129
+ results.push({
130
+ testId,
131
+ ...test,
132
+ status,
133
+ report: {
134
+ duration: testResult.duration,
135
+ ema: stats.ema,
136
+ fails: stats.history.filter((h) => h.status === TestStatus.Failed).length,
137
+ title: testResult.title,
138
+ lastSuccessfulRunTimestamp: stats.history.findLast((h) => h.status === TestStatus.Passed)?.updated,
139
+ },
140
+ });
141
+ await writeFile(file, JSON.stringify(results, null, 2));
142
+ await release();
143
+ }
144
+ async updateHistory(status, { test, testResult, config }) {
145
+ const file = this.getHistoryRunPath();
146
+ const release = await lock(file, { retries: 100 });
147
+ const history = JSON.parse(await readFile(file, 'utf-8'));
148
+ const id = this.getTestId({ ...test, ...testResult });
149
+ const item = history[id];
150
+ const itemCopy = structuredClone(item);
151
+ item.history.push({
152
+ duration: testResult.duration,
153
+ status,
154
+ updated: Date.now(),
155
+ });
156
+ if (item.history.length > config.historyWindow) {
157
+ item.history.splice(0, item.history.length - config.historyWindow);
158
+ }
159
+ item.ema = this.calculateEMA(testResult.duration, item.ema, config.historyWindow);
160
+ await writeFile(file, JSON.stringify(history, null, 2));
161
+ await release();
162
+ return itemCopy;
163
+ }
164
+ getRunIdFilePath(runId) {
165
+ return `${this.dir}/${runId}.queue.json`;
166
+ }
167
+ getRunConfigPath(runId) {
168
+ return `${this.dir}/${runId}.config.json`;
169
+ }
170
+ getHistoryRunPath() {
171
+ return `${this.dir}/tests.history.json`;
172
+ }
173
+ getResultsRunPath(runId) {
174
+ return `${this.dir}/${runId}.results.json`;
175
+ }
176
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@commander-js/extra-typings';
2
+ import { CreateArgs } from './create-args.js';
3
+ import { FileAdapter } from './file-adapter.js';
4
+ export declare function factory(args: CreateArgs): Promise<FileAdapter>;
5
+ export declare function createOptions(command: Command): void;
6
+ export declare const description = "Local file storage adapter";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import { Option } from '@commander-js/extra-typings';
2
+ import { FileAdapter } from './file-adapter.js';
3
+ export async function factory(args) {
4
+ return new FileAdapter(args);
5
+ }
6
+ export function createOptions(command) {
7
+ command.addOption(new Option('--directory <string>', 'Directory to store test run data').default('test-runs').env('DIRECTORY'));
8
+ }
9
+ export const description = 'Local file storage adapter';
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@dionlarson/playwright-orchestrator-file",
3
+ "version": "1.3.0",
4
+ "keywords": [],
5
+ "author": "Rostyslav Kudrevatykh",
6
+ "license": "Apache-2.0",
7
+ "description": "Playwright orchestrator local file plugin",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/dionlarson/playwright-orchestrator-private.git"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "dependencies": {
16
+ "@commander-js/extra-typings": "^13.0.0",
17
+ "@dionlarson/playwright-orchestrator-core": "^1.3.0",
18
+ "commander": "^13.0.0",
19
+ "proper-lockfile": "4.1.2"
20
+ },
21
+ "devDependencies": {
22
+ "@types/proper-lockfile": "^4.1.2"
23
+ },
24
+ "main": "dist/index.js",
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "default": "./dist/index.js"
29
+ }
30
+ }
31
+ }