@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.
@@ -1,17 +1,23 @@
1
- import { TestItem, TestRunInfo, Adapter, TestRunConfig } from '@playwright-orchestrator/core';
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(runId: string, test: TestItem): Promise<void>;
10
- failTest(runId: string, test: TestItem): Promise<void>;
11
- private updateTestStatus;
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(runId, test) {
56
- await this.updateTestStatus(runId, test, TestStatus.Passed);
58
+ async finishTest(params) {
59
+ await this.updateTestWithResults(TestStatus.Passed, params);
57
60
  }
58
- async failTest(runId, test) {
59
- await this.updateTestStatus(runId, test, TestStatus.Failed);
61
+ async failTest(params) {
62
+ await this.updateTestWithResults(TestStatus.Failed, params);
60
63
  }
61
- async updateTestStatus(runId, test, status) {
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
- const { updated, status, config } = result.rows[0];
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwright-orchestrator/pg",
3
- "version": "1.1.4",
3
+ "version": "1.2.1",
4
4
  "keywords": [],
5
5
  "author": "Rostyslav Kudrevatykh",
6
6
  "license": "Apache-2.0",