@playwright-orchestrator/pg 1.2.0 → 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/dist/index.d.ts +3 -3
- package/dist/index.js +19 -8
- package/dist/pg-initializer.d.ts +9 -0
- package/dist/pg-initializer.js +97 -0
- package/dist/pg-pool.d.ts +7 -0
- package/dist/pg-pool.js +46 -0
- package/dist/pg-shard-handler.d.ts +15 -0
- package/dist/pg-shard-handler.js +134 -0
- package/dist/pg-test-run-creator.d.ts +14 -0
- package/dist/pg-test-run-creator.js +127 -0
- package/dist/postgresql-adapter.d.ts +7 -15
- package/dist/postgresql-adapter.js +93 -278
- package/dist/symbols.d.ts +2 -0
- package/dist/symbols.js +2 -0
- package/package.json +31 -32
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from '@commander-js/extra-typings';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
export declare function
|
|
2
|
+
import { Container } from 'inversify';
|
|
3
|
+
import type { CreateArgs } from './create-args.js';
|
|
4
|
+
export declare function register(container: Container, options: CreateArgs): Promise<void>;
|
|
5
5
|
export declare function createOptions(command: Command): void;
|
|
6
6
|
export declare const description = "PostgreSQL storage adapter";
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { Option } from '@commander-js/extra-typings';
|
|
2
|
+
import { SYMBOLS } from '@playwright-orchestrator/core';
|
|
2
3
|
import { PostgreSQLAdapter } from './postgresql-adapter.js';
|
|
4
|
+
import { PgShardHandler } from './pg-shard-handler.js';
|
|
5
|
+
import { PgInitializer } from './pg-initializer.js';
|
|
6
|
+
import { PgTestRunCreator } from './pg-test-run-creator.js';
|
|
7
|
+
import { PgPool } from './pg-pool.js';
|
|
3
8
|
import { readFile } from 'node:fs/promises';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
import { PG_CONFIG, PG_POOL } from './symbols.js';
|
|
10
|
+
export async function register(container, options) {
|
|
11
|
+
if (options.sslCa) {
|
|
12
|
+
options.sslCa = await readFile(options.sslCa);
|
|
7
13
|
}
|
|
8
|
-
if (
|
|
9
|
-
|
|
14
|
+
if (options.sslCert) {
|
|
15
|
+
options.sslCert = await readFile(options.sslCert);
|
|
10
16
|
}
|
|
11
|
-
if (
|
|
12
|
-
|
|
17
|
+
if (options.sslKey) {
|
|
18
|
+
options.sslKey = await readFile(options.sslKey);
|
|
13
19
|
}
|
|
14
|
-
|
|
20
|
+
container.bind(PG_CONFIG).toConstantValue(options);
|
|
21
|
+
container.bind(PG_POOL).to(PgPool).inSingletonScope();
|
|
22
|
+
container.bind(SYMBOLS.Adapter).to(PostgreSQLAdapter).inSingletonScope();
|
|
23
|
+
container.bind(SYMBOLS.ShardHandler).to(PgShardHandler).inSingletonScope();
|
|
24
|
+
container.bind(SYMBOLS.Initializer).to(PgInitializer).inSingletonScope();
|
|
25
|
+
container.bind(SYMBOLS.TestRunCreator).to(PgTestRunCreator).inSingletonScope();
|
|
15
26
|
}
|
|
16
27
|
export function createOptions(command) {
|
|
17
28
|
command
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Initializer } from '@playwright-orchestrator/core';
|
|
2
|
+
import type { CreateArgs } from './create-args.js';
|
|
3
|
+
import { PgPool } from './pg-pool.js';
|
|
4
|
+
export declare class PgInitializer implements Initializer {
|
|
5
|
+
private readonly config;
|
|
6
|
+
private readonly pgPool;
|
|
7
|
+
constructor(config: CreateArgs, pgPool: PgPool);
|
|
8
|
+
initialize(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { injectable, inject } from 'inversify';
|
|
14
|
+
import { PgPool } from './pg-pool.js';
|
|
15
|
+
import pg from 'pg';
|
|
16
|
+
import { PG_CONFIG, PG_POOL } from './symbols.js';
|
|
17
|
+
let PgInitializer = class PgInitializer {
|
|
18
|
+
config;
|
|
19
|
+
pgPool;
|
|
20
|
+
constructor(config, pgPool) {
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.pgPool = pgPool;
|
|
23
|
+
}
|
|
24
|
+
async initialize() {
|
|
25
|
+
const { tableNamePrefix } = this.config;
|
|
26
|
+
const configTable = pg.escapeIdentifier(`${tableNamePrefix}_test_runs`);
|
|
27
|
+
const testsTable = pg.escapeIdentifier(`${tableNamePrefix}_tests`);
|
|
28
|
+
const testInfoTable = pg.escapeIdentifier(`${tableNamePrefix}_test_info`);
|
|
29
|
+
const testInfoHistoryTable = pg.escapeIdentifier(`${tableNamePrefix}_test_info_history`);
|
|
30
|
+
await this.pgPool.pool.query(`CREATE TABLE IF NOT EXISTS ${configTable} (
|
|
31
|
+
id UUID PRIMARY KEY,
|
|
32
|
+
status INT NOT NULL,
|
|
33
|
+
updated TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
34
|
+
config JSONB NOT NULL
|
|
35
|
+
);
|
|
36
|
+
CREATE TABLE IF NOT EXISTS ${testsTable} (
|
|
37
|
+
run_id UUID NOT NULL,
|
|
38
|
+
order_num INT NOT NULL,
|
|
39
|
+
status INT NOT NULL DEFAULT 0,
|
|
40
|
+
test_id TEXT NOT NULL,
|
|
41
|
+
file TEXT NOT NULL,
|
|
42
|
+
line INT NOT NULL,
|
|
43
|
+
character INT NOT NULL,
|
|
44
|
+
projects JSONB NOT NULL,
|
|
45
|
+
timeout INT NOT NULL,
|
|
46
|
+
ema FLOAT NOT NULL,
|
|
47
|
+
updated TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
48
|
+
report JSONB,
|
|
49
|
+
children JSONB,
|
|
50
|
+
PRIMARY KEY (run_id, order_num),
|
|
51
|
+
FOREIGN KEY (run_id) REFERENCES ${configTable}(id)
|
|
52
|
+
);
|
|
53
|
+
ALTER TABLE ${testsTable} ADD COLUMN IF NOT EXISTS children JSONB;
|
|
54
|
+
ALTER TABLE ${testsTable} ADD COLUMN IF NOT EXISTS ema FLOAT NOT NULL DEFAULT 0;
|
|
55
|
+
ALTER TABLE ${testsTable} ALTER COLUMN ema DROP DEFAULT;
|
|
56
|
+
ALTER TABLE ${testsTable} ADD COLUMN IF NOT EXISTS test_id TEXT NOT NULL DEFAULT '';
|
|
57
|
+
ALTER TABLE ${testsTable} ALTER COLUMN test_id DROP DEFAULT;
|
|
58
|
+
DO $$
|
|
59
|
+
BEGIN
|
|
60
|
+
IF NOT EXISTS (
|
|
61
|
+
SELECT 1
|
|
62
|
+
FROM information_schema.columns
|
|
63
|
+
WHERE table_name = ${pg.escapeLiteral(`${tableNamePrefix}_tests`)} AND column_name = 'projects'
|
|
64
|
+
) THEN
|
|
65
|
+
ALTER TABLE ${testsTable} ADD COLUMN projects JSONB NOT NULL DEFAULT '[]';
|
|
66
|
+
UPDATE ${testsTable} SET projects = jsonb_build_array(project) WHERE project IS NOT NULL;
|
|
67
|
+
ALTER TABLE ${testsTable} DROP COLUMN IF EXISTS project;
|
|
68
|
+
ALTER TABLE ${testsTable} ALTER COLUMN projects DROP DEFAULT;
|
|
69
|
+
END IF;
|
|
70
|
+
END $$;
|
|
71
|
+
UPDATE ${testsTable} SET projects = '[]' WHERE projects IS NULL;
|
|
72
|
+
CREATE INDEX IF NOT EXISTS status_idx ON ${testsTable}(status);
|
|
73
|
+
CREATE TABLE IF NOT EXISTS ${testInfoTable} (
|
|
74
|
+
id SERIAL PRIMARY KEY,
|
|
75
|
+
name TEXT NOT NULL,
|
|
76
|
+
ema FLOAT NOT NULL DEFAULT 0,
|
|
77
|
+
created TIMESTAMP NOT NULL DEFAULT NOW()
|
|
78
|
+
);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS name_idx ON ${testInfoTable} USING HASH (name);
|
|
80
|
+
CREATE TABLE IF NOT EXISTS ${testInfoHistoryTable} (
|
|
81
|
+
id SERIAL PRIMARY KEY,
|
|
82
|
+
duration FLOAT NOT NULL,
|
|
83
|
+
status INT NOT NULL,
|
|
84
|
+
updated TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
85
|
+
test_info_id INT NOT NULL,
|
|
86
|
+
FOREIGN KEY (test_info_id) REFERENCES ${testInfoTable}(id)
|
|
87
|
+
);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS test_info_id_idx ON ${testInfoHistoryTable}(test_info_id);`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
PgInitializer = __decorate([
|
|
92
|
+
injectable(),
|
|
93
|
+
__param(0, inject(PG_CONFIG)),
|
|
94
|
+
__param(1, inject(PG_POOL)),
|
|
95
|
+
__metadata("design:paramtypes", [Object, PgPool])
|
|
96
|
+
], PgInitializer);
|
|
97
|
+
export { PgInitializer };
|
package/dist/pg-pool.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { injectable, inject, preDestroy } from 'inversify';
|
|
14
|
+
import pg from 'pg';
|
|
15
|
+
import { PG_CONFIG } from './symbols.js';
|
|
16
|
+
let PgPool = class PgPool {
|
|
17
|
+
pool;
|
|
18
|
+
constructor({ connectionString, sslCa, sslCert, sslKey }) {
|
|
19
|
+
const config = { connectionString };
|
|
20
|
+
config.ssl = sslCa || sslCert || sslKey ? {} : undefined;
|
|
21
|
+
if (sslCa)
|
|
22
|
+
config.ssl.ca = sslCa;
|
|
23
|
+
if (sslCert && sslKey) {
|
|
24
|
+
config.ssl.cert = sslCert;
|
|
25
|
+
config.ssl.key = sslKey;
|
|
26
|
+
}
|
|
27
|
+
this.pool = new pg.Pool(config);
|
|
28
|
+
}
|
|
29
|
+
async dispose() {
|
|
30
|
+
if (this.pool.ending)
|
|
31
|
+
return;
|
|
32
|
+
await this.pool.end();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
__decorate([
|
|
36
|
+
preDestroy(),
|
|
37
|
+
__metadata("design:type", Function),
|
|
38
|
+
__metadata("design:paramtypes", []),
|
|
39
|
+
__metadata("design:returntype", Promise)
|
|
40
|
+
], PgPool.prototype, "dispose", null);
|
|
41
|
+
PgPool = __decorate([
|
|
42
|
+
injectable(),
|
|
43
|
+
__param(0, inject(PG_CONFIG)),
|
|
44
|
+
__metadata("design:paramtypes", [Object])
|
|
45
|
+
], PgPool);
|
|
46
|
+
export { PgPool };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ShardHandler } from '@playwright-orchestrator/core';
|
|
2
|
+
import type { TestItem, TestRunConfig } from '@playwright-orchestrator/core';
|
|
3
|
+
import type { CreateArgs } from './create-args.js';
|
|
4
|
+
import { PgPool } from './pg-pool.js';
|
|
5
|
+
export declare class PgShardHandler implements ShardHandler {
|
|
6
|
+
private readonly configTable;
|
|
7
|
+
private readonly testsTable;
|
|
8
|
+
private readonly pool;
|
|
9
|
+
constructor({ tableNamePrefix }: CreateArgs, pgPool: PgPool);
|
|
10
|
+
getNextTest(runId: string, _config: TestRunConfig): Promise<TestItem | undefined>;
|
|
11
|
+
getNextTestByProject(runId: string, project: string): Promise<TestItem | undefined>;
|
|
12
|
+
private claimNextTest;
|
|
13
|
+
startShard(runId: string): Promise<TestRunConfig>;
|
|
14
|
+
finishShard(runId: string): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { injectable, inject } from 'inversify';
|
|
14
|
+
import { RunStatus, TestStatus } from '@playwright-orchestrator/core';
|
|
15
|
+
import { PgPool } from './pg-pool.js';
|
|
16
|
+
import pg from 'pg';
|
|
17
|
+
import { PG_CONFIG, PG_POOL } from './symbols.js';
|
|
18
|
+
let PgShardHandler = class PgShardHandler {
|
|
19
|
+
configTable;
|
|
20
|
+
testsTable;
|
|
21
|
+
pool;
|
|
22
|
+
constructor({ tableNamePrefix }, pgPool) {
|
|
23
|
+
this.pool = pgPool.pool;
|
|
24
|
+
this.configTable = pg.escapeIdentifier(`${tableNamePrefix}_test_runs`);
|
|
25
|
+
this.testsTable = pg.escapeIdentifier(`${tableNamePrefix}_tests`);
|
|
26
|
+
}
|
|
27
|
+
async getNextTest(runId, _config) {
|
|
28
|
+
return this.claimNextTest(runId);
|
|
29
|
+
}
|
|
30
|
+
async getNextTestByProject(runId, project) {
|
|
31
|
+
return this.claimNextTest(runId, project);
|
|
32
|
+
}
|
|
33
|
+
async claimNextTest(runId, project) {
|
|
34
|
+
const projectFilter = project ? `AND projects @> to_jsonb(ARRAY[$4]::text[])` : '';
|
|
35
|
+
const values = project
|
|
36
|
+
? [runId, TestStatus.Ready, TestStatus.Ongoing, project]
|
|
37
|
+
: [runId, TestStatus.Ready, TestStatus.Ongoing];
|
|
38
|
+
const client = await this.pool.connect();
|
|
39
|
+
try {
|
|
40
|
+
await client.query('BEGIN');
|
|
41
|
+
const result = await client.query({
|
|
42
|
+
text: `WITH next_test AS (
|
|
43
|
+
SELECT order_num FROM ${this.testsTable}
|
|
44
|
+
WHERE run_id = $1 AND status = $2 ${projectFilter}
|
|
45
|
+
ORDER BY order_num
|
|
46
|
+
LIMIT 1
|
|
47
|
+
FOR UPDATE SKIP LOCKED
|
|
48
|
+
)
|
|
49
|
+
UPDATE ${this.testsTable} t
|
|
50
|
+
SET status = $3, updated = NOW()
|
|
51
|
+
FROM next_test
|
|
52
|
+
WHERE t.run_id = $1 AND t.order_num = next_test.order_num
|
|
53
|
+
RETURNING *`,
|
|
54
|
+
values,
|
|
55
|
+
});
|
|
56
|
+
await client.query('COMMIT');
|
|
57
|
+
if (result.rowCount === 0)
|
|
58
|
+
return undefined;
|
|
59
|
+
const { file, line, character, projects, timeout, ema, order_num, children, test_id } = result.rows[0];
|
|
60
|
+
return {
|
|
61
|
+
file,
|
|
62
|
+
position: `${line}:${character}`,
|
|
63
|
+
projects,
|
|
64
|
+
timeout,
|
|
65
|
+
ema,
|
|
66
|
+
order: order_num,
|
|
67
|
+
children,
|
|
68
|
+
testId: test_id,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
await client.query('ROLLBACK');
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
client.release();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async startShard(runId) {
|
|
80
|
+
const client = await this.pool.connect();
|
|
81
|
+
try {
|
|
82
|
+
await client.query('BEGIN');
|
|
83
|
+
let result = await client.query({
|
|
84
|
+
text: `SELECT * FROM ${this.configTable} WHERE id = $1 FOR UPDATE`,
|
|
85
|
+
values: [runId],
|
|
86
|
+
});
|
|
87
|
+
if (result.rowCount === 0) {
|
|
88
|
+
throw new Error(`Run ${runId} not found`);
|
|
89
|
+
}
|
|
90
|
+
const { updated: updatedBefore, status: statusBefore } = result.rows[0];
|
|
91
|
+
if (statusBefore === RunStatus.Created || statusBefore === RunStatus.Finished) {
|
|
92
|
+
await client.query({
|
|
93
|
+
text: `UPDATE ${this.testsTable}
|
|
94
|
+
SET updated = NOW(), status = $3
|
|
95
|
+
WHERE run_id = $1 AND status = $2 AND updated <= $4;`,
|
|
96
|
+
values: [runId, TestStatus.Failed, TestStatus.Ready, updatedBefore],
|
|
97
|
+
});
|
|
98
|
+
result = await client.query({
|
|
99
|
+
text: `UPDATE ${this.configTable}
|
|
100
|
+
SET status = (CASE
|
|
101
|
+
WHEN status = $2 THEN ${RunStatus.Run}
|
|
102
|
+
ELSE ${RunStatus.RepeatRun}
|
|
103
|
+
END),
|
|
104
|
+
updated = NOW()
|
|
105
|
+
WHERE id = $1
|
|
106
|
+
RETURNING *;`,
|
|
107
|
+
values: [runId, RunStatus.Created],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
await client.query('COMMIT');
|
|
111
|
+
return result.rows[0].config;
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
await client.query('ROLLBACK');
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
client.release();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async finishShard(runId) {
|
|
122
|
+
await this.pool.query({
|
|
123
|
+
text: `UPDATE ${this.configTable} SET status = $1, updated = NOW() WHERE id = $2`,
|
|
124
|
+
values: [RunStatus.Finished, runId],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
PgShardHandler = __decorate([
|
|
129
|
+
injectable(),
|
|
130
|
+
__param(0, inject(PG_CONFIG)),
|
|
131
|
+
__param(1, inject(PG_POOL)),
|
|
132
|
+
__metadata("design:paramtypes", [Object, PgPool])
|
|
133
|
+
], PgShardHandler);
|
|
134
|
+
export { PgShardHandler };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseTestRunCreator } from '@playwright-orchestrator/core';
|
|
2
|
+
import type { TestItem, TestRun, TestSortItem } from '@playwright-orchestrator/core';
|
|
3
|
+
import type { CreateArgs } from './create-args.js';
|
|
4
|
+
import { PgPool } from './pg-pool.js';
|
|
5
|
+
export declare class PgTestRunCreator extends BaseTestRunCreator {
|
|
6
|
+
private readonly configTable;
|
|
7
|
+
private readonly testsTable;
|
|
8
|
+
private readonly testInfoTable;
|
|
9
|
+
private readonly testInfoHistoryTable;
|
|
10
|
+
private readonly pool;
|
|
11
|
+
constructor({ tableNamePrefix }: CreateArgs, pgPool: PgPool);
|
|
12
|
+
loadTestInfos(tests: TestItem[]): Promise<Map<string, TestSortItem>>;
|
|
13
|
+
saveRunData(runId: string, run: TestRun, tests: TestItem[]): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { injectable, inject, injectFromBase } from 'inversify';
|
|
14
|
+
import { BaseTestRunCreator, RunStatus, TestStatus } from '@playwright-orchestrator/core';
|
|
15
|
+
import { PgPool } from './pg-pool.js';
|
|
16
|
+
import pg from 'pg';
|
|
17
|
+
import { PG_CONFIG, PG_POOL } from './symbols.js';
|
|
18
|
+
let PgTestRunCreator = class PgTestRunCreator extends BaseTestRunCreator {
|
|
19
|
+
configTable;
|
|
20
|
+
testsTable;
|
|
21
|
+
testInfoTable;
|
|
22
|
+
testInfoHistoryTable;
|
|
23
|
+
pool;
|
|
24
|
+
constructor({ tableNamePrefix }, pgPool) {
|
|
25
|
+
super();
|
|
26
|
+
this.pool = pgPool.pool;
|
|
27
|
+
this.configTable = pg.escapeIdentifier(`${tableNamePrefix}_test_runs`);
|
|
28
|
+
this.testsTable = pg.escapeIdentifier(`${tableNamePrefix}_tests`);
|
|
29
|
+
this.testInfoTable = pg.escapeIdentifier(`${tableNamePrefix}_test_info`);
|
|
30
|
+
this.testInfoHistoryTable = pg.escapeIdentifier(`${tableNamePrefix}_test_info_history`);
|
|
31
|
+
}
|
|
32
|
+
async loadTestInfos(tests) {
|
|
33
|
+
const results = await this.pool.query({
|
|
34
|
+
text: `
|
|
35
|
+
WITH test_names AS (
|
|
36
|
+
SELECT UNNEST($1::TEXT[]) AS name
|
|
37
|
+
),
|
|
38
|
+
existing_tests AS (
|
|
39
|
+
SELECT id, name, ema, created FROM ${this.testInfoTable}
|
|
40
|
+
WHERE name IN (SELECT name FROM test_names)
|
|
41
|
+
),
|
|
42
|
+
inserted_tests AS (
|
|
43
|
+
INSERT INTO ${this.testInfoTable} (name)
|
|
44
|
+
SELECT name FROM test_names
|
|
45
|
+
WHERE NOT EXISTS (
|
|
46
|
+
SELECT 1 FROM ${this.testInfoTable} WHERE name = test_names.name
|
|
47
|
+
)
|
|
48
|
+
RETURNING id, name, ema, created
|
|
49
|
+
),
|
|
50
|
+
combined_tests AS (
|
|
51
|
+
SELECT * FROM existing_tests
|
|
52
|
+
UNION ALL
|
|
53
|
+
SELECT * FROM inserted_tests
|
|
54
|
+
)
|
|
55
|
+
SELECT
|
|
56
|
+
t.id,
|
|
57
|
+
t.name,
|
|
58
|
+
t.ema,
|
|
59
|
+
t.created,
|
|
60
|
+
COUNT(CASE WHEN h.status = ${TestStatus.Failed} THEN 1 END) as fails
|
|
61
|
+
FROM combined_tests t
|
|
62
|
+
LEFT JOIN ${this.testInfoHistoryTable} h ON h.test_info_id = t.id
|
|
63
|
+
GROUP BY t.id, t.name, t.ema, t.created`,
|
|
64
|
+
values: [tests.map((t) => t.testId)],
|
|
65
|
+
});
|
|
66
|
+
const testInfo = new Map();
|
|
67
|
+
for (const { name, ema, fails } of results.rows) {
|
|
68
|
+
testInfo.set(name, { ema, fails: +fails });
|
|
69
|
+
}
|
|
70
|
+
return testInfo;
|
|
71
|
+
}
|
|
72
|
+
async saveRunData(runId, run, tests) {
|
|
73
|
+
const client = await this.pool.connect();
|
|
74
|
+
try {
|
|
75
|
+
await client.query('BEGIN');
|
|
76
|
+
await client.query({
|
|
77
|
+
text: `INSERT INTO ${this.configTable} (id, status, config) VALUES ($1, $2, $3)`,
|
|
78
|
+
values: [runId, RunStatus.Created, JSON.stringify(run.config)],
|
|
79
|
+
});
|
|
80
|
+
if (tests.length > 0) {
|
|
81
|
+
const fields = ['order_num', 'file', 'line', 'character', 'projects', 'timeout', 'ema', 'children', 'test_id'];
|
|
82
|
+
await client.query({
|
|
83
|
+
text: `INSERT INTO ${this.testsTable} (run_id, ${fields.join(', ')}) VALUES ${tests
|
|
84
|
+
.map((_, i) => {
|
|
85
|
+
const len = fields.length;
|
|
86
|
+
const values = fields.map((_, j) => `$${i * len + j + 2}`).join(', ');
|
|
87
|
+
return `($1, ${values})`;
|
|
88
|
+
})
|
|
89
|
+
.join(', ')}`,
|
|
90
|
+
values: [
|
|
91
|
+
runId,
|
|
92
|
+
...tests.flatMap(({ position, order, file, projects, timeout, ema, children, testId }) => {
|
|
93
|
+
const [line, character] = position.split(':');
|
|
94
|
+
return [
|
|
95
|
+
order,
|
|
96
|
+
file,
|
|
97
|
+
line,
|
|
98
|
+
character,
|
|
99
|
+
JSON.stringify(projects),
|
|
100
|
+
timeout,
|
|
101
|
+
ema,
|
|
102
|
+
children != null ? JSON.stringify(children) : null,
|
|
103
|
+
testId,
|
|
104
|
+
];
|
|
105
|
+
}),
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
await client.query('COMMIT');
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
await client.query('ROLLBACK');
|
|
113
|
+
throw e;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
client.release();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
PgTestRunCreator = __decorate([
|
|
121
|
+
injectable(),
|
|
122
|
+
injectFromBase({ extendProperties: true, extendConstructorArguments: false }),
|
|
123
|
+
__param(0, inject(PG_CONFIG)),
|
|
124
|
+
__param(1, inject(PG_POOL)),
|
|
125
|
+
__metadata("design:paramtypes", [Object, PgPool])
|
|
126
|
+
], PgTestRunCreator);
|
|
127
|
+
export { PgTestRunCreator };
|
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { CreateArgs } from './create-args.js';
|
|
3
|
-
import {
|
|
4
|
-
export declare class PostgreSQLAdapter extends
|
|
1
|
+
import { BaseAdapter, TestRunReport, SaveTestResultParams } from '@playwright-orchestrator/core';
|
|
2
|
+
import type { CreateArgs } from './create-args.js';
|
|
3
|
+
import { PgPool } from './pg-pool.js';
|
|
4
|
+
export declare class PostgreSQLAdapter extends BaseAdapter {
|
|
5
5
|
private readonly configTable;
|
|
6
6
|
private readonly testsTable;
|
|
7
7
|
private readonly testInfoTable;
|
|
8
8
|
private readonly testInfoHistoryTable;
|
|
9
9
|
private readonly pool;
|
|
10
|
-
constructor({
|
|
11
|
-
getNextTest(runId: string, config: TestRunConfig): Promise<TestItem | undefined>;
|
|
12
|
-
finishTest(params: ResultTestParams): Promise<void>;
|
|
13
|
-
failTest(params: ResultTestParams): Promise<void>;
|
|
14
|
-
saveTestRun({ runId, args, historyWindow, testRun }: SaveTestRunParams): Promise<void>;
|
|
15
|
-
initialize(): Promise<void>;
|
|
16
|
-
startShard(runId: string): Promise<TestRunConfig>;
|
|
17
|
-
finishShard(runId: string): Promise<void>;
|
|
18
|
-
dispose(): Promise<void>;
|
|
10
|
+
constructor({ tableNamePrefix }: CreateArgs, pgPool: PgPool);
|
|
19
11
|
getReportData(runId: string): Promise<TestRunReport>;
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
getTestEma(testId: string): Promise<number>;
|
|
13
|
+
saveTestResult({ runId, test, item, historyWindow, newEma }: SaveTestResultParams): Promise<void>;
|
|
22
14
|
private mapConfig;
|
|
23
15
|
}
|
|
@@ -1,196 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { BaseAdapter, TestStatus, } from '@playwright-orchestrator/core';
|
|
14
|
+
import { injectable, inject } from 'inversify';
|
|
15
|
+
import { PgPool } from './pg-pool.js';
|
|
2
16
|
import pg from 'pg';
|
|
3
|
-
|
|
17
|
+
import { PG_CONFIG, PG_POOL } from './symbols.js';
|
|
18
|
+
let PostgreSQLAdapter = class PostgreSQLAdapter extends BaseAdapter {
|
|
4
19
|
configTable;
|
|
5
20
|
testsTable;
|
|
6
21
|
testInfoTable;
|
|
7
22
|
testInfoHistoryTable;
|
|
8
23
|
pool;
|
|
9
|
-
constructor({
|
|
24
|
+
constructor({ tableNamePrefix }, pgPool) {
|
|
10
25
|
super();
|
|
11
|
-
|
|
12
|
-
config.ssl = sslCa || sslCert || sslKey ? {} : undefined;
|
|
13
|
-
if (sslCa) {
|
|
14
|
-
config.ssl.ca = sslCa;
|
|
15
|
-
}
|
|
16
|
-
if (sslCert && sslKey) {
|
|
17
|
-
config.ssl.cert = sslCert;
|
|
18
|
-
config.ssl.key = sslKey;
|
|
19
|
-
}
|
|
20
|
-
this.pool = new pg.Pool(config);
|
|
26
|
+
this.pool = pgPool.pool;
|
|
21
27
|
this.configTable = pg.escapeIdentifier(`${tableNamePrefix}_test_runs`);
|
|
22
28
|
this.testsTable = pg.escapeIdentifier(`${tableNamePrefix}_tests`);
|
|
23
29
|
this.testInfoTable = pg.escapeIdentifier(`${tableNamePrefix}_test_info`);
|
|
24
30
|
this.testInfoHistoryTable = pg.escapeIdentifier(`${tableNamePrefix}_test_info_history`);
|
|
25
31
|
}
|
|
26
|
-
async getNextTest(runId, config) {
|
|
27
|
-
const client = await this.pool.connect();
|
|
28
|
-
try {
|
|
29
|
-
await client.query('BEGIN');
|
|
30
|
-
const result = await client.query({
|
|
31
|
-
text: `WITH next_test AS (
|
|
32
|
-
SELECT order_num FROM ${this.testsTable}
|
|
33
|
-
WHERE run_id = $1 AND status = $2
|
|
34
|
-
ORDER BY order_num
|
|
35
|
-
LIMIT 1
|
|
36
|
-
FOR UPDATE SKIP LOCKED
|
|
37
|
-
)
|
|
38
|
-
UPDATE ${this.testsTable} t
|
|
39
|
-
SET status = $3, updated = NOW()
|
|
40
|
-
FROM next_test
|
|
41
|
-
WHERE t.run_id = $1 AND t.order_num = next_test.order_num
|
|
42
|
-
RETURNING *`,
|
|
43
|
-
values: [runId, TestStatus.Ready, TestStatus.Ongoing],
|
|
44
|
-
});
|
|
45
|
-
await client.query('COMMIT');
|
|
46
|
-
if (result.rowCount === 0)
|
|
47
|
-
return undefined;
|
|
48
|
-
const { file, line, character, project, timeout, order_num } = result.rows[0];
|
|
49
|
-
return { file, position: `${line}:${character}`, project, timeout, order: order_num };
|
|
50
|
-
}
|
|
51
|
-
catch (e) {
|
|
52
|
-
await this.pool.query('ROLLBACK');
|
|
53
|
-
}
|
|
54
|
-
finally {
|
|
55
|
-
client.release();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async finishTest(params) {
|
|
59
|
-
await this.updateTestWithResults(TestStatus.Passed, params);
|
|
60
|
-
}
|
|
61
|
-
async failTest(params) {
|
|
62
|
-
await this.updateTestWithResults(TestStatus.Failed, params);
|
|
63
|
-
}
|
|
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 });
|
|
68
|
-
await this.pool.query({
|
|
69
|
-
text: `INSERT INTO ${this.configTable} (id, status, config) VALUES ($1, $2, $3)`,
|
|
70
|
-
values: [runId, RunStatus.Created, JSON.stringify({ ...testRun.config, args, historyWindow })],
|
|
71
|
-
});
|
|
72
|
-
const fields = ['order_num', 'file', 'line', 'character', 'project', 'timeout'];
|
|
73
|
-
await this.pool.query({
|
|
74
|
-
text: `INSERT INTO ${this.testsTable} (run_id, ${fields.join(', ')}) VALUES ${tests
|
|
75
|
-
.map((_, i) => {
|
|
76
|
-
const len = fields.length;
|
|
77
|
-
const values = fields.map((_, j) => `$${i * len + j + 2}`).join(', ');
|
|
78
|
-
return `($1, ${values})`;
|
|
79
|
-
})
|
|
80
|
-
.join(', ')}`,
|
|
81
|
-
values: [
|
|
82
|
-
runId,
|
|
83
|
-
...tests.flatMap(({ position, order, file, project, timeout }) => {
|
|
84
|
-
const [line, character] = position.split(':');
|
|
85
|
-
return [order, file, line, character, project, timeout];
|
|
86
|
-
}),
|
|
87
|
-
],
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
async initialize() {
|
|
91
|
-
await this.pool.query(`CREATE TABLE IF NOT EXISTS ${this.configTable} (
|
|
92
|
-
id UUID PRIMARY KEY,
|
|
93
|
-
status INT NOT NULL,
|
|
94
|
-
updated TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
95
|
-
config JSONB NOT NULL
|
|
96
|
-
);
|
|
97
|
-
CREATE TABLE IF NOT EXISTS ${this.testsTable} (
|
|
98
|
-
run_id UUID NOT NULL,
|
|
99
|
-
order_num INT NOT NULL,
|
|
100
|
-
status INT NOT NULL DEFAULT 0,
|
|
101
|
-
file TEXT NOT NULL,
|
|
102
|
-
line INT NOT NULL,
|
|
103
|
-
character INT NOT NULL,
|
|
104
|
-
project TEXT NOT NULL,
|
|
105
|
-
timeout INT NOT NULL,
|
|
106
|
-
updated TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
107
|
-
report JSONB,
|
|
108
|
-
PRIMARY KEY (run_id, order_num),
|
|
109
|
-
FOREIGN KEY (run_id) REFERENCES ${this.configTable}(id)
|
|
110
|
-
);
|
|
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);`);
|
|
128
|
-
}
|
|
129
|
-
async startShard(runId) {
|
|
130
|
-
const client = await this.pool.connect();
|
|
131
|
-
try {
|
|
132
|
-
await client.query('BEGIN');
|
|
133
|
-
let result = await client.query({
|
|
134
|
-
text: `
|
|
135
|
-
SELECT *
|
|
136
|
-
FROM ${this.configTable}
|
|
137
|
-
WHERE id = $1
|
|
138
|
-
FOR UPDATE`,
|
|
139
|
-
values: [runId],
|
|
140
|
-
});
|
|
141
|
-
if (result.rowCount === 0) {
|
|
142
|
-
throw new Error(`Run ${runId} not found`);
|
|
143
|
-
}
|
|
144
|
-
const { updated: updatedBefore, status: statusBefore } = result.rows[0];
|
|
145
|
-
if (statusBefore === RunStatus.Created || statusBefore === RunStatus.Finished) {
|
|
146
|
-
await client.query({
|
|
147
|
-
text: `
|
|
148
|
-
UPDATE ${this.testsTable}
|
|
149
|
-
SET updated = NOW(), status = $3
|
|
150
|
-
WHERE run_id = $1 AND status = $2 AND updated <= $4;`,
|
|
151
|
-
values: [runId, TestStatus.Failed, TestStatus.Ready, updatedBefore],
|
|
152
|
-
});
|
|
153
|
-
// using str interpolation for case statement to avoid casting ints to strings
|
|
154
|
-
result = await client.query({
|
|
155
|
-
text: `
|
|
156
|
-
UPDATE ${this.configTable}
|
|
157
|
-
SET status = (CASE
|
|
158
|
-
WHEN status = $2 THEN ${RunStatus.Run}
|
|
159
|
-
ELSE ${RunStatus.RepeatRun}
|
|
160
|
-
END),
|
|
161
|
-
updated = NOW()
|
|
162
|
-
WHERE id = $1
|
|
163
|
-
RETURNING *;`,
|
|
164
|
-
values: [runId, RunStatus.Created],
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
await client.query('COMMIT');
|
|
168
|
-
return this.mapConfig(result.rows[0]);
|
|
169
|
-
}
|
|
170
|
-
catch (e) {
|
|
171
|
-
await client.query('ROLLBACK');
|
|
172
|
-
throw e;
|
|
173
|
-
}
|
|
174
|
-
finally {
|
|
175
|
-
client.release();
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
async finishShard(runId) {
|
|
179
|
-
// set 'updated' field to current time as test run exhausted all tests
|
|
180
|
-
// update 'updated' field until last shard set correct finish time
|
|
181
|
-
await this.pool.query({
|
|
182
|
-
text: `UPDATE ${this.configTable}
|
|
183
|
-
SET status = $1,
|
|
184
|
-
updated = NOW()
|
|
185
|
-
WHERE id = $2`,
|
|
186
|
-
values: [RunStatus.Finished, runId],
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
async dispose() {
|
|
190
|
-
if (this.pool.ending)
|
|
191
|
-
return;
|
|
192
|
-
await this.pool.end();
|
|
193
|
-
}
|
|
194
32
|
async getReportData(runId) {
|
|
195
33
|
const { rows: [dbConfig], } = await this.pool.query({
|
|
196
34
|
text: `SELECT * FROM ${this.configTable} WHERE id = $1`,
|
|
@@ -205,117 +43,87 @@ export class PostgreSQLAdapter extends Adapter {
|
|
|
205
43
|
return {
|
|
206
44
|
runId,
|
|
207
45
|
config: this.mapConfig(dbConfig),
|
|
208
|
-
tests: rows.map(({ file,
|
|
209
|
-
averageDuration: 0,
|
|
46
|
+
tests: rows.map(({ file, projects, line, character, report }) => ({
|
|
47
|
+
averageDuration: report?.ema ?? 0,
|
|
210
48
|
duration: report?.duration ?? 0,
|
|
211
49
|
status: report?.status ?? TestStatus.Ready,
|
|
212
50
|
fails: report?.fails ?? 0,
|
|
213
51
|
file,
|
|
214
52
|
position: `${line}:${character}`,
|
|
215
|
-
|
|
53
|
+
projects,
|
|
216
54
|
title: report?.title,
|
|
217
55
|
lastSuccessfulRunTimestamp: report?.lastSuccessfulRun,
|
|
218
56
|
})),
|
|
219
57
|
};
|
|
220
58
|
}
|
|
221
|
-
async
|
|
222
|
-
const testId = this.getTestId({ ...test, ...testResult });
|
|
59
|
+
async getTestEma(testId) {
|
|
223
60
|
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`,
|
|
61
|
+
text: `SELECT ema FROM ${this.testInfoTable} WHERE name = $1`,
|
|
238
62
|
values: [testId],
|
|
239
63
|
});
|
|
240
|
-
|
|
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
|
-
});
|
|
64
|
+
return testInfo?.ema ?? 0;
|
|
279
65
|
}
|
|
280
|
-
async
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
66
|
+
async saveTestResult({ runId, test, item, historyWindow, newEma }) {
|
|
67
|
+
const client = await this.pool.connect();
|
|
68
|
+
try {
|
|
69
|
+
await client.query('BEGIN');
|
|
70
|
+
const { rows: [{ id }], } = await client.query({
|
|
71
|
+
text: `UPDATE ${this.testInfoTable} SET ema = $1 WHERE name = $2 RETURNING id`,
|
|
72
|
+
values: [newEma, test.testId],
|
|
73
|
+
});
|
|
74
|
+
await client.query({
|
|
75
|
+
text: `INSERT INTO ${this.testInfoHistoryTable} (status, duration, updated, test_info_id) VALUES ($1, $2, NOW(), $3)`,
|
|
76
|
+
values: [item.status, item.duration, id],
|
|
77
|
+
});
|
|
78
|
+
await client.query({
|
|
79
|
+
text: `DELETE FROM ${this.testInfoHistoryTable}
|
|
80
|
+
WHERE id IN (
|
|
81
|
+
SELECT id
|
|
82
|
+
FROM ${this.testInfoHistoryTable}
|
|
83
|
+
WHERE test_info_id = $1
|
|
84
|
+
ORDER BY updated
|
|
85
|
+
LIMIT GREATEST(0, (SELECT COUNT(*) FROM ${this.testInfoHistoryTable} WHERE test_info_id = $1) - $2)
|
|
86
|
+
)`,
|
|
87
|
+
values: [id, historyWindow],
|
|
88
|
+
});
|
|
89
|
+
const { rows } = await client.query({
|
|
90
|
+
text: `SELECT status, duration, EXTRACT(EPOCH FROM updated) * 1000 AS updated
|
|
91
|
+
FROM ${this.testInfoHistoryTable} WHERE test_info_id = $1 ORDER BY updated`,
|
|
92
|
+
values: [id],
|
|
93
|
+
});
|
|
94
|
+
const history = rows.map(({ status, duration, updated }) => ({
|
|
95
|
+
status: +status,
|
|
96
|
+
duration,
|
|
97
|
+
updated: +updated,
|
|
98
|
+
}));
|
|
99
|
+
const report = this.buildReport(test, item, newEma, history);
|
|
100
|
+
await client.query({
|
|
101
|
+
text: `UPDATE ${this.testsTable}
|
|
102
|
+
SET status = $1, updated = NOW(), report = $2
|
|
103
|
+
WHERE run_id = $3 AND order_num = $4`,
|
|
104
|
+
values: [
|
|
105
|
+
report.status,
|
|
106
|
+
{
|
|
107
|
+
title: report.title,
|
|
108
|
+
status: report.status,
|
|
109
|
+
duration: report.duration,
|
|
110
|
+
ema: report.averageDuration,
|
|
111
|
+
fails: report.fails,
|
|
112
|
+
lastSuccessfulRun: report.lastSuccessfulRunTimestamp,
|
|
113
|
+
},
|
|
114
|
+
runId,
|
|
115
|
+
test.order,
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
await client.query('COMMIT');
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
await client.query('ROLLBACK');
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
client.release();
|
|
317
126
|
}
|
|
318
|
-
return testInfo;
|
|
319
127
|
}
|
|
320
128
|
mapConfig(dbConfig) {
|
|
321
129
|
return {
|
|
@@ -324,4 +132,11 @@ export class PostgreSQLAdapter extends Adapter {
|
|
|
324
132
|
status: dbConfig.status,
|
|
325
133
|
};
|
|
326
134
|
}
|
|
327
|
-
}
|
|
135
|
+
};
|
|
136
|
+
PostgreSQLAdapter = __decorate([
|
|
137
|
+
injectable(),
|
|
138
|
+
__param(0, inject(PG_CONFIG)),
|
|
139
|
+
__param(1, inject(PG_POOL)),
|
|
140
|
+
__metadata("design:paramtypes", [Object, PgPool])
|
|
141
|
+
], PostgreSQLAdapter);
|
|
142
|
+
export { PostgreSQLAdapter };
|
package/dist/symbols.js
ADDED
package/package.json
CHANGED
|
@@ -1,34 +1,33 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"prepare": "cp ../../LICENSE.md ./"
|
|
2
|
+
"name": "@playwright-orchestrator/pg",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"keywords": [],
|
|
5
|
+
"author": "Rostyslav Kudrevatykh",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"description": "Playwright orchestrator PostgreSQL plugin",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/rostmanrk/playwright-orchestrator.git"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@commander-js/extra-typings": "^13.0.0",
|
|
17
|
+
"commander": "^13.0.0",
|
|
18
|
+
"inversify": "^8.0.0-beta.0",
|
|
19
|
+
"pg": "^8.13.1",
|
|
20
|
+
"@playwright-orchestrator/core": "1.3.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/pg": "^8.11.0"
|
|
24
|
+
},
|
|
25
|
+
"main": "dist/index.js",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"default": "./dist/index.js"
|
|
33
30
|
}
|
|
34
|
-
}
|
|
31
|
+
},
|
|
32
|
+
"scripts": {}
|
|
33
|
+
}
|