@playwright-orchestrator/pg 1.1.4 → 1.2.1
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/dist/postgresql-adapter.d.ts +11 -5
- package/dist/postgresql-adapter.js +166 -25
- package/package.json +1 -1
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import { TestItem,
|
|
1
|
+
import { TestItem, Adapter, TestRunConfig, ResultTestParams, SaveTestRunParams } from '@playwright-orchestrator/core';
|
|
2
2
|
import { CreateArgs } from './create-args.js';
|
|
3
|
+
import { TestRunReport } from '../../core/dist/types/reporter.js';
|
|
3
4
|
export declare class PostgreSQLAdapter extends Adapter {
|
|
4
5
|
private readonly configTable;
|
|
5
6
|
private readonly testsTable;
|
|
7
|
+
private readonly testInfoTable;
|
|
8
|
+
private readonly testInfoHistoryTable;
|
|
6
9
|
private readonly pool;
|
|
7
10
|
constructor({ connectionString, tableNamePrefix, sslCa, sslCert, sslKey }: CreateArgs);
|
|
8
11
|
getNextTest(runId: string, config: TestRunConfig): Promise<TestItem | undefined>;
|
|
9
|
-
finishTest(
|
|
10
|
-
failTest(
|
|
11
|
-
|
|
12
|
-
saveTestRun(runId: string, testRun: TestRunInfo, args: string[]): Promise<void>;
|
|
12
|
+
finishTest(params: ResultTestParams): Promise<void>;
|
|
13
|
+
failTest(params: ResultTestParams): Promise<void>;
|
|
14
|
+
saveTestRun({ runId, args, historyWindow, testRun }: SaveTestRunParams): Promise<void>;
|
|
13
15
|
initialize(): Promise<void>;
|
|
14
16
|
startShard(runId: string): Promise<TestRunConfig>;
|
|
15
17
|
finishShard(runId: string): Promise<void>;
|
|
16
18
|
dispose(): Promise<void>;
|
|
19
|
+
getReportData(runId: string): Promise<TestRunReport>;
|
|
20
|
+
private updateTestWithResults;
|
|
21
|
+
private loadTestInfos;
|
|
22
|
+
private mapConfig;
|
|
17
23
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { Adapter, RunStatus, TestStatus } from '@playwright-orchestrator/core';
|
|
1
|
+
import { Adapter, RunStatus, TestStatus, } from '@playwright-orchestrator/core';
|
|
2
2
|
import pg from 'pg';
|
|
3
3
|
export class PostgreSQLAdapter extends Adapter {
|
|
4
4
|
configTable;
|
|
5
5
|
testsTable;
|
|
6
|
+
testInfoTable;
|
|
7
|
+
testInfoHistoryTable;
|
|
6
8
|
pool;
|
|
7
9
|
constructor({ connectionString, tableNamePrefix, sslCa, sslCert, sslKey }) {
|
|
8
10
|
super();
|
|
@@ -18,13 +20,14 @@ export class PostgreSQLAdapter extends Adapter {
|
|
|
18
20
|
this.pool = new pg.Pool(config);
|
|
19
21
|
this.configTable = pg.escapeIdentifier(`${tableNamePrefix}_test_runs`);
|
|
20
22
|
this.testsTable = pg.escapeIdentifier(`${tableNamePrefix}_tests`);
|
|
23
|
+
this.testInfoTable = pg.escapeIdentifier(`${tableNamePrefix}_test_info`);
|
|
24
|
+
this.testInfoHistoryTable = pg.escapeIdentifier(`${tableNamePrefix}_test_info_history`);
|
|
21
25
|
}
|
|
22
26
|
async getNextTest(runId, config) {
|
|
23
27
|
const client = await this.pool.connect();
|
|
24
28
|
try {
|
|
25
29
|
await client.query('BEGIN');
|
|
26
30
|
const result = await client.query({
|
|
27
|
-
name: 'select-next-test',
|
|
28
31
|
text: `WITH next_test AS (
|
|
29
32
|
SELECT order_num FROM ${this.testsTable}
|
|
30
33
|
WHERE run_id = $1 AND status = $2
|
|
@@ -52,31 +55,22 @@ export class PostgreSQLAdapter extends Adapter {
|
|
|
52
55
|
client.release();
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
|
-
async finishTest(
|
|
56
|
-
await this.
|
|
58
|
+
async finishTest(params) {
|
|
59
|
+
await this.updateTestWithResults(TestStatus.Passed, params);
|
|
57
60
|
}
|
|
58
|
-
async failTest(
|
|
59
|
-
await this.
|
|
61
|
+
async failTest(params) {
|
|
62
|
+
await this.updateTestWithResults(TestStatus.Failed, params);
|
|
60
63
|
}
|
|
61
|
-
async
|
|
64
|
+
async saveTestRun({ runId, args, historyWindow, testRun }) {
|
|
65
|
+
let tests = this.transformTestRunToItems(testRun.testRun);
|
|
66
|
+
const testInfos = await this.loadTestInfos(tests);
|
|
67
|
+
tests = this.sortTests(tests, testInfos, { historyWindow });
|
|
62
68
|
await this.pool.query({
|
|
63
|
-
name: 'update-test-status',
|
|
64
|
-
text: `UPDATE ${this.testsTable}
|
|
65
|
-
SET status = $1, updated = NOW()
|
|
66
|
-
WHERE run_id = $2 AND order_num = $3`,
|
|
67
|
-
values: [status, runId, test.order],
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
async saveTestRun(runId, testRun, args) {
|
|
71
|
-
await this.pool.query({
|
|
72
|
-
name: 'insert-config',
|
|
73
69
|
text: `INSERT INTO ${this.configTable} (id, status, config) VALUES ($1, $2, $3)`,
|
|
74
|
-
values: [runId, RunStatus.Created, JSON.stringify({ ...testRun.config, args })],
|
|
70
|
+
values: [runId, RunStatus.Created, JSON.stringify({ ...testRun.config, args, historyWindow })],
|
|
75
71
|
});
|
|
76
|
-
const tests = this.flattenTestRun(testRun.testRun);
|
|
77
72
|
const fields = ['order_num', 'file', 'line', 'character', 'project', 'timeout'];
|
|
78
73
|
await this.pool.query({
|
|
79
|
-
name: 'insert-tests',
|
|
80
74
|
text: `INSERT INTO ${this.testsTable} (run_id, ${fields.join(', ')}) VALUES ${tests
|
|
81
75
|
.map((_, i) => {
|
|
82
76
|
const len = fields.length;
|
|
@@ -110,10 +104,27 @@ export class PostgreSQLAdapter extends Adapter {
|
|
|
110
104
|
project TEXT NOT NULL,
|
|
111
105
|
timeout INT NOT NULL,
|
|
112
106
|
updated TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
107
|
+
report JSONB,
|
|
113
108
|
PRIMARY KEY (run_id, order_num),
|
|
114
109
|
FOREIGN KEY (run_id) REFERENCES ${this.configTable}(id)
|
|
115
110
|
);
|
|
116
|
-
CREATE INDEX IF NOT EXISTS status_idx ON ${this.testsTable}(status)
|
|
111
|
+
CREATE INDEX IF NOT EXISTS status_idx ON ${this.testsTable}(status);
|
|
112
|
+
CREATE TABLE IF NOT EXISTS ${this.testInfoTable} (
|
|
113
|
+
id SERIAL PRIMARY KEY,
|
|
114
|
+
name TEXT NOT NULL,
|
|
115
|
+
ema FLOAT NOT NULL DEFAULT 0,
|
|
116
|
+
created TIMESTAMP NOT NULL DEFAULT NOW()
|
|
117
|
+
);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS name_idx ON ${this.testInfoTable} USING HASH (name);
|
|
119
|
+
CREATE TABLE IF NOT EXISTS ${this.testInfoHistoryTable} (
|
|
120
|
+
id SERIAL PRIMARY KEY,
|
|
121
|
+
duration FLOAT NOT NULL,
|
|
122
|
+
status INT NOT NULL,
|
|
123
|
+
updated TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
124
|
+
test_info_id INT NOT NULL,
|
|
125
|
+
FOREIGN KEY (test_info_id) REFERENCES ${this.testInfoTable}(id)
|
|
126
|
+
);
|
|
127
|
+
CREATE INDEX IF NOT EXISTS test_info_id_idx ON ${this.testInfoHistoryTable}(test_info_id);`);
|
|
117
128
|
}
|
|
118
129
|
async startShard(runId) {
|
|
119
130
|
const client = await this.pool.connect();
|
|
@@ -154,9 +165,7 @@ export class PostgreSQLAdapter extends Adapter {
|
|
|
154
165
|
});
|
|
155
166
|
}
|
|
156
167
|
await client.query('COMMIT');
|
|
157
|
-
|
|
158
|
-
const mappedConfig = { ...config, updated: updated.getTime(), status };
|
|
159
|
-
return mappedConfig;
|
|
168
|
+
return this.mapConfig(result.rows[0]);
|
|
160
169
|
}
|
|
161
170
|
catch (e) {
|
|
162
171
|
await client.query('ROLLBACK');
|
|
@@ -170,7 +179,6 @@ export class PostgreSQLAdapter extends Adapter {
|
|
|
170
179
|
// set 'updated' field to current time as test run exhausted all tests
|
|
171
180
|
// update 'updated' field until last shard set correct finish time
|
|
172
181
|
await this.pool.query({
|
|
173
|
-
name: 'update-finish-config',
|
|
174
182
|
text: `UPDATE ${this.configTable}
|
|
175
183
|
SET status = $1,
|
|
176
184
|
updated = NOW()
|
|
@@ -183,4 +191,137 @@ export class PostgreSQLAdapter extends Adapter {
|
|
|
183
191
|
return;
|
|
184
192
|
await this.pool.end();
|
|
185
193
|
}
|
|
194
|
+
async getReportData(runId) {
|
|
195
|
+
const { rows: [dbConfig], } = await this.pool.query({
|
|
196
|
+
text: `SELECT * FROM ${this.configTable} WHERE id = $1`,
|
|
197
|
+
values: [runId],
|
|
198
|
+
});
|
|
199
|
+
if (!dbConfig)
|
|
200
|
+
throw new Error(`Run ${runId} not found`);
|
|
201
|
+
const { rows } = await this.pool.query({
|
|
202
|
+
text: `SELECT * FROM ${this.testsTable} WHERE run_id = $1`,
|
|
203
|
+
values: [runId],
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
runId,
|
|
207
|
+
config: this.mapConfig(dbConfig),
|
|
208
|
+
tests: rows.map(({ file, project, line, character, report }) => ({
|
|
209
|
+
averageDuration: 0,
|
|
210
|
+
duration: report?.duration ?? 0,
|
|
211
|
+
status: report?.status ?? TestStatus.Ready,
|
|
212
|
+
fails: report?.fails ?? 0,
|
|
213
|
+
file,
|
|
214
|
+
position: `${line}:${character}`,
|
|
215
|
+
project,
|
|
216
|
+
title: report?.title,
|
|
217
|
+
lastSuccessfulRunTimestamp: report?.lastSuccessfulRun,
|
|
218
|
+
})),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
async updateTestWithResults(status, { runId, test, config, testResult }) {
|
|
222
|
+
const testId = this.getTestId({ ...test, ...testResult });
|
|
223
|
+
const { rows: [testInfo], } = await this.pool.query({
|
|
224
|
+
text: `SELECT
|
|
225
|
+
id,
|
|
226
|
+
ema,
|
|
227
|
+
(
|
|
228
|
+
SELECT COUNT(*) FROM ${this.testInfoHistoryTable}
|
|
229
|
+
WHERE status = ${TestStatus.Failed} AND test_info_id = info.id
|
|
230
|
+
) AS fails,
|
|
231
|
+
(
|
|
232
|
+
SELECT updated FROM ${this.testInfoHistoryTable}
|
|
233
|
+
WHERE status = ${TestStatus.Passed} AND test_info_id = info.id
|
|
234
|
+
ORDER BY updated DESC LIMIT 1
|
|
235
|
+
) AS last_successful_run
|
|
236
|
+
FROM ${this.testInfoTable} info
|
|
237
|
+
WHERE name = $1`,
|
|
238
|
+
values: [testId],
|
|
239
|
+
});
|
|
240
|
+
const report = {
|
|
241
|
+
title: testResult.title,
|
|
242
|
+
status,
|
|
243
|
+
duration: testResult.duration,
|
|
244
|
+
ema: testInfo.ema,
|
|
245
|
+
fails: +testInfo.fails,
|
|
246
|
+
lastSuccessfulRun: testInfo.last_successful_run?.getTime?.(),
|
|
247
|
+
};
|
|
248
|
+
const newEma = this.calculateEMA(testResult.duration, testInfo.ema, config.historyWindow);
|
|
249
|
+
await this.pool.query({
|
|
250
|
+
text: `UPDATE ${this.testsTable}
|
|
251
|
+
SET
|
|
252
|
+
status = $1,
|
|
253
|
+
updated = NOW(),
|
|
254
|
+
report = $2
|
|
255
|
+
WHERE run_id = $3 AND order_num = $4;`,
|
|
256
|
+
values: [status, report, runId, test.order],
|
|
257
|
+
});
|
|
258
|
+
await this.pool.query({
|
|
259
|
+
text: `UPDATE ${this.testInfoTable} SET ema = $1 WHERE id = $2;`,
|
|
260
|
+
values: [newEma, testInfo.id],
|
|
261
|
+
});
|
|
262
|
+
await this.pool.query({
|
|
263
|
+
text: `INSERT INTO ${this.testInfoHistoryTable} (status, duration, updated, test_info_id)
|
|
264
|
+
VALUES ($1, $2, NOW(), $3);`,
|
|
265
|
+
values: [status, testResult.duration, testInfo.id],
|
|
266
|
+
});
|
|
267
|
+
await this.pool.query({
|
|
268
|
+
text: `DELETE FROM ${this.testInfoHistoryTable}
|
|
269
|
+
WHERE id IN (
|
|
270
|
+
SELECT id
|
|
271
|
+
FROM ${this.testInfoHistoryTable}
|
|
272
|
+
WHERE test_info_id = $1
|
|
273
|
+
ORDER BY updated
|
|
274
|
+
LIMIT 10
|
|
275
|
+
OFFSET 10
|
|
276
|
+
)`,
|
|
277
|
+
values: [testInfo.id],
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
async loadTestInfos(tests) {
|
|
281
|
+
const results = await this.pool.query({
|
|
282
|
+
text: `
|
|
283
|
+
WITH test_names AS (
|
|
284
|
+
SELECT UNNEST($1::TEXT[]) AS name
|
|
285
|
+
),
|
|
286
|
+
existing_tests AS (
|
|
287
|
+
SELECT id, name, ema, created FROM ${this.testInfoTable}
|
|
288
|
+
WHERE name IN (SELECT name FROM test_names)
|
|
289
|
+
),
|
|
290
|
+
inserted_tests AS (
|
|
291
|
+
INSERT INTO ${this.testInfoTable} (name)
|
|
292
|
+
SELECT name FROM test_names
|
|
293
|
+
WHERE NOT EXISTS (
|
|
294
|
+
SELECT 1 FROM ${this.testInfoTable} WHERE name = test_names.name
|
|
295
|
+
)
|
|
296
|
+
RETURNING id, name, ema, created
|
|
297
|
+
),
|
|
298
|
+
combined_tests AS (
|
|
299
|
+
SELECT * FROM existing_tests
|
|
300
|
+
UNION ALL
|
|
301
|
+
SELECT * FROM inserted_tests
|
|
302
|
+
)
|
|
303
|
+
SELECT
|
|
304
|
+
t.id,
|
|
305
|
+
t.name,
|
|
306
|
+
t.ema,
|
|
307
|
+
t.created,
|
|
308
|
+
COUNT(CASE WHEN h.status = ${TestStatus.Failed} THEN 1 END) as fails
|
|
309
|
+
FROM combined_tests t
|
|
310
|
+
LEFT JOIN ${this.testInfoHistoryTable} h ON h.test_info_id = t.id
|
|
311
|
+
GROUP BY t.id, t.name, t.ema, t.created`,
|
|
312
|
+
values: [tests.map((t) => t.testId)],
|
|
313
|
+
});
|
|
314
|
+
const testInfo = new Map();
|
|
315
|
+
for (const { name, ema, fails } of results.rows) {
|
|
316
|
+
testInfo.set(name, { ema, fails: +fails });
|
|
317
|
+
}
|
|
318
|
+
return testInfo;
|
|
319
|
+
}
|
|
320
|
+
mapConfig(dbConfig) {
|
|
321
|
+
return {
|
|
322
|
+
...dbConfig.config,
|
|
323
|
+
updated: dbConfig.updated.getTime(),
|
|
324
|
+
status: dbConfig.status,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
186
327
|
}
|