@creative-introvert/tons-of-tests 2.1.0 → 3.0.0-rc.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/README.md +143 -37
- package/dist/cjs/Classify.js +1 -2
- package/dist/cjs/Classify.js.map +1 -1
- package/dist/cjs/RepositoryError.js +42 -33
- package/dist/cjs/RepositoryError.js.map +1 -1
- package/dist/cjs/Show.js +18 -0
- package/dist/cjs/Show.js.map +1 -1
- package/dist/cjs/Test.js +40 -2
- package/dist/cjs/Test.js.map +1 -1
- package/dist/cjs/Test.repository.js +25 -22
- package/dist/cjs/Test.repository.js.map +1 -1
- package/dist/cjs/TestRun.js +28 -0
- package/dist/cjs/TestRun.js.map +1 -0
- package/dist/cjs/TestValueError.js +23 -0
- package/dist/cjs/TestValueError.js.map +1 -0
- package/dist/cjs/index.js +0 -7
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/internal/Classify.js +1 -9
- package/dist/cjs/internal/Classify.js.map +1 -1
- package/dist/cjs/internal/Test.js +235 -95
- package/dist/cjs/internal/Test.js.map +1 -1
- package/dist/cjs/internal/Test.repository.sqlite.js +487 -356
- package/dist/cjs/internal/Test.repository.sqlite.js.map +1 -1
- package/dist/cjs/internal/Test.serialization.js +128 -0
- package/dist/cjs/internal/Test.serialization.js.map +1 -0
- package/dist/cjs/internal/common.js.map +1 -1
- package/dist/cjs/internal/stats.js +29 -1
- package/dist/cjs/internal/stats.js.map +1 -1
- package/dist/cjs/internal/summarize.js +143 -29
- package/dist/cjs/internal/summarize.js.map +1 -1
- package/dist/cjs/internal/test-repository-fixtures.js +31 -0
- package/dist/cjs/internal/test-repository-fixtures.js.map +1 -0
- package/dist/cjs/internal/test-repository-schema.js +98 -0
- package/dist/cjs/internal/test-repository-schema.js.map +1 -0
- package/dist/cjs/internal/test-result-stats.js +89 -0
- package/dist/cjs/internal/test-result-stats.js.map +1 -0
- package/dist/dts/Classify.d.ts +0 -1
- package/dist/dts/Classify.d.ts.map +1 -1
- package/dist/dts/RepositoryError.d.ts +47 -20
- package/dist/dts/RepositoryError.d.ts.map +1 -1
- package/dist/dts/Show.d.ts +5 -5
- package/dist/dts/Show.d.ts.map +1 -1
- package/dist/dts/Test.d.ts +136 -13
- package/dist/dts/Test.d.ts.map +1 -1
- package/dist/dts/Test.repository.d.ts +132 -9
- package/dist/dts/Test.repository.d.ts.map +1 -1
- package/dist/dts/TestRun.d.ts +22 -0
- package/dist/dts/TestRun.d.ts.map +1 -0
- package/dist/dts/TestValueError.d.ts +17 -0
- package/dist/dts/TestValueError.d.ts.map +1 -0
- package/dist/dts/index.d.ts +0 -1
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/internal/Classify.d.ts +0 -1
- package/dist/dts/internal/Classify.d.ts.map +1 -1
- package/dist/dts/internal/Test.d.ts +64 -30
- package/dist/dts/internal/Test.d.ts.map +1 -1
- package/dist/dts/internal/Test.repository.sqlite.d.ts +92 -11
- package/dist/dts/internal/Test.repository.sqlite.d.ts.map +1 -1
- package/dist/dts/internal/Test.serialization.d.ts +34 -0
- package/dist/dts/internal/Test.serialization.d.ts.map +1 -0
- package/dist/dts/internal/common.d.ts +1 -1
- package/dist/dts/internal/common.d.ts.map +1 -1
- package/dist/dts/internal/stats.d.ts +16 -1
- package/dist/dts/internal/stats.d.ts.map +1 -1
- package/dist/dts/internal/summarize.d.ts +39 -7
- package/dist/dts/internal/summarize.d.ts.map +1 -1
- package/dist/dts/internal/test-repository-fixtures.d.ts +33 -0
- package/dist/dts/internal/test-repository-fixtures.d.ts.map +1 -0
- package/dist/dts/internal/test-repository-schema.d.ts +16 -0
- package/dist/dts/internal/test-repository-schema.d.ts.map +1 -0
- package/dist/dts/internal/test-result-stats.d.ts +7 -0
- package/dist/dts/internal/test-result-stats.d.ts.map +1 -0
- package/dist/esm/Classify.js +0 -1
- package/dist/esm/Classify.js.map +1 -1
- package/dist/esm/RepositoryError.js +29 -29
- package/dist/esm/RepositoryError.js.map +1 -1
- package/dist/esm/Show.js +2 -2
- package/dist/esm/Show.js.map +1 -1
- package/dist/esm/Test.js +37 -1
- package/dist/esm/Test.js.map +1 -1
- package/dist/esm/Test.repository.js +26 -21
- package/dist/esm/Test.repository.js.map +1 -1
- package/dist/esm/TestRun.js +22 -0
- package/dist/esm/TestRun.js.map +1 -0
- package/dist/esm/TestValueError.js +16 -0
- package/dist/esm/TestValueError.js.map +1 -0
- package/dist/esm/index.js +0 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/Classify.js +0 -7
- package/dist/esm/internal/Classify.js.map +1 -1
- package/dist/esm/internal/Test.js +231 -94
- package/dist/esm/internal/Test.js.map +1 -1
- package/dist/esm/internal/Test.repository.sqlite.js +486 -356
- package/dist/esm/internal/Test.repository.sqlite.js.map +1 -1
- package/dist/esm/internal/Test.serialization.js +119 -0
- package/dist/esm/internal/Test.serialization.js.map +1 -0
- package/dist/esm/internal/common.js.map +1 -1
- package/dist/esm/internal/stats.js +27 -0
- package/dist/esm/internal/stats.js.map +1 -1
- package/dist/esm/internal/summarize.js +143 -28
- package/dist/esm/internal/summarize.js.map +1 -1
- package/dist/esm/internal/test-repository-fixtures.js +25 -0
- package/dist/esm/internal/test-repository-fixtures.js.map +1 -0
- package/dist/esm/internal/test-repository-schema.js +92 -0
- package/dist/esm/internal/test-repository-schema.js.map +1 -0
- package/dist/esm/internal/test-result-stats.js +83 -0
- package/dist/esm/internal/test-result-stats.js.map +1 -0
- package/package.json +1 -1
- package/dist/cjs/Test.repository.types.js +0 -14
- package/dist/cjs/Test.repository.types.js.map +0 -1
- package/dist/cjs/internal/lib/Schema.js +0 -14
- package/dist/cjs/internal/lib/Schema.js.map +0 -1
- package/dist/dts/Test.repository.types.d.ts +0 -62
- package/dist/dts/Test.repository.types.d.ts.map +0 -1
- package/dist/dts/internal/lib/Schema.d.ts +0 -4
- package/dist/dts/internal/lib/Schema.d.ts.map +0 -1
- package/dist/esm/Test.repository.types.js +0 -7
- package/dist/esm/Test.repository.types.js.map +0 -1
- package/dist/esm/internal/lib/Schema.js +0 -7
- package/dist/esm/internal/lib/Schema.js.map +0 -1
|
@@ -1,387 +1,517 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import { Array as A,
|
|
3
|
-
import {
|
|
4
|
-
import { TestRun } from '../
|
|
5
|
-
|
|
6
|
-
import { RepositoryDecodeError, RepositoryQueryError, RepositoryWriteError, TestRunNotCreated } from '../RepositoryError.js';
|
|
1
|
+
import * as Sqlite from '@effect/sql-sqlite-node';
|
|
2
|
+
import { Array as A, Context, Crypto, Effect, Exit, FileSystem, Layer, Option, Path, Ref, Schema, Scope, Semaphore, Stream, TxReentrantLock } from 'effect';
|
|
3
|
+
import { SqlClient } from 'effect/sql';
|
|
4
|
+
import { TestRun } from '../TestRun.js';
|
|
5
|
+
import { RepositoryPathConflict, TestRunDuplicate, TestRunIncomplete, TestRunNotCreated, TestRunNotFound } from '../RepositoryError.js';
|
|
7
6
|
import { LabelSchema } from './Classify.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const TestResultWriteSchema = /*#__PURE__*/Schema.Struct({
|
|
7
|
+
import { checkFormat, initializeSchema, tables } from './test-repository-schema.js';
|
|
8
|
+
import { withStats } from './test-result-stats.js';
|
|
9
|
+
import { makeResultCodec, stringifyJson } from './Test.serialization.js';
|
|
10
|
+
export { tables } from './test-repository-schema.js';
|
|
11
|
+
export class TestRepositoryStorage extends /*#__PURE__*/Context.Service()('@creative-introvert/tons-of-tests/internal/TestRepositoryStorage') {}
|
|
12
|
+
export const TestResultStorage = /*#__PURE__*/Schema.Struct({
|
|
15
13
|
id: Schema.String,
|
|
16
14
|
hashTestCase: Schema.String,
|
|
17
15
|
key: Schema.String,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
expected: /*#__PURE__*/Schema.fromJsonString(Schema.Unknown),
|
|
23
|
-
label: LabelSchema,
|
|
24
|
-
tags: /*#__PURE__*/split(','),
|
|
25
|
-
timeMillis: Schema.Finite
|
|
16
|
+
input: /*#__PURE__*/Schema.fromJsonString(Schema.Json),
|
|
17
|
+
result: /*#__PURE__*/Schema.fromJsonString(Schema.Json),
|
|
18
|
+
expected: /*#__PURE__*/Schema.fromJsonString(Schema.Json),
|
|
19
|
+
label: LabelSchema
|
|
26
20
|
});
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
count
|
|
33
|
-
}]) => count,
|
|
34
|
-
encode: count => [{
|
|
35
|
-
count
|
|
36
|
-
}]
|
|
37
|
-
})));
|
|
38
|
-
const TestRunsReadSchema = /*#__PURE__*/Schema.Array(TestRun);
|
|
39
|
-
const TestRunResultsSchema = /*#__PURE__*/Schema.Array(/*#__PURE__*/Schema.Struct({
|
|
40
|
-
testRun: Schema.Int,
|
|
41
|
-
testResult: Schema.String,
|
|
21
|
+
const StoredResult = /*#__PURE__*/Schema.Struct({
|
|
22
|
+
...TestResultStorage.fields,
|
|
23
|
+
input: Schema.String,
|
|
24
|
+
result: Schema.String,
|
|
25
|
+
expected: Schema.String,
|
|
42
26
|
ordering: Schema.Int,
|
|
43
27
|
resultOrdering: Schema.Int,
|
|
28
|
+
tags: /*#__PURE__*/Schema.fromJsonString(/*#__PURE__*/Schema.Array(Schema.String)),
|
|
44
29
|
timeMillis: Schema.Finite
|
|
30
|
+
});
|
|
31
|
+
const RawResult = /*#__PURE__*/StoredResult.mapFields(fields => ({
|
|
32
|
+
...fields,
|
|
33
|
+
tags: Schema.String
|
|
45
34
|
}));
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
}))
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (!resultColumns.some(column => column.name === 'key')) {
|
|
97
|
-
yield* sql`
|
|
98
|
-
ALTER TABLE ${sql(tables.testResults)}
|
|
99
|
-
ADD COLUMN key TEXT NOT NULL DEFAULT 'result';
|
|
100
|
-
`;
|
|
101
|
-
}
|
|
102
|
-
if (!resultColumns.some(column => column.name === 'resultOrdering')) {
|
|
103
|
-
yield* sql`
|
|
104
|
-
ALTER TABLE ${sql(tables.testResults)}
|
|
105
|
-
ADD COLUMN resultOrdering INTEGER NOT NULL DEFAULT 0;
|
|
106
|
-
`;
|
|
35
|
+
const StoredRun = /*#__PURE__*/Schema.Union([/*#__PURE__*/TestRun.members[0].pipe(/*#__PURE__*/Schema.fieldsAssign({
|
|
36
|
+
executionId: Schema.String
|
|
37
|
+
})), /*#__PURE__*/TestRun.members[1].pipe(/*#__PURE__*/Schema.fieldsAssign({
|
|
38
|
+
executionId: Schema.String
|
|
39
|
+
}))]);
|
|
40
|
+
const encodeResult = /*#__PURE__*/Schema.encodeEffect(StoredResult);
|
|
41
|
+
const decodeRows = /*#__PURE__*/Schema.decodeUnknownEffect(/*#__PURE__*/Schema.Array(StoredResult));
|
|
42
|
+
const decodeRawRows = /*#__PURE__*/Schema.decodeUnknownEffect(/*#__PURE__*/Schema.Array(RawResult));
|
|
43
|
+
const decodeRuns = /*#__PURE__*/Schema.decodeUnknownEffect(/*#__PURE__*/Schema.Array(TestRun));
|
|
44
|
+
const decodeStoredRuns = /*#__PURE__*/Schema.decodeUnknownEffect(/*#__PURE__*/Schema.Array(StoredRun));
|
|
45
|
+
const decodeListedRuns = /*#__PURE__*/Schema.decodeUnknownEffect(/*#__PURE__*/Schema.Array(/*#__PURE__*/TestRun.members[1].pipe(/*#__PURE__*/Schema.fieldsAssign({
|
|
46
|
+
isBaseline: /*#__PURE__*/Schema.Literals([0, 1])
|
|
47
|
+
}))));
|
|
48
|
+
const decodeVariantBaselines = /*#__PURE__*/Schema.decodeUnknownEffect(/*#__PURE__*/Schema.Array(/*#__PURE__*/Schema.Struct({
|
|
49
|
+
variant: Schema.String,
|
|
50
|
+
id: /*#__PURE__*/Schema.NullOr(Schema.Int),
|
|
51
|
+
name: /*#__PURE__*/Schema.NullOr(Schema.String),
|
|
52
|
+
state: /*#__PURE__*/Schema.NullOr(/*#__PURE__*/Schema.Literal('Complete'))
|
|
53
|
+
})));
|
|
54
|
+
const decodeRunLimit = /*#__PURE__*/Schema.decodeUnknownEffect(/*#__PURE__*/Schema.Int.check(/*#__PURE__*/Schema.isBetween({
|
|
55
|
+
minimum: 1,
|
|
56
|
+
maximum: 100
|
|
57
|
+
})));
|
|
58
|
+
const openClient = options => Layer.build(Sqlite.SqliteClient.layer(options)).pipe(Effect.map(Context.get(SqlClient.SqlClient)));
|
|
59
|
+
export const makeTestRepository = /*#__PURE__*/Effect.fnUntraced(function* (options) {
|
|
60
|
+
const fs = yield* FileSystem.FileSystem;
|
|
61
|
+
const path = yield* Path.Path;
|
|
62
|
+
const crypto = yield* Crypto.Crypto;
|
|
63
|
+
const lifetime = yield* Effect.scope;
|
|
64
|
+
const memoryLock = yield* TxReentrantLock.make();
|
|
65
|
+
const snapshotLock = yield* Semaphore.make(1);
|
|
66
|
+
const snapshotMemory = yield* Ref.make(Option.none());
|
|
67
|
+
const canonicalPath = filename => fs.realPath(filename).pipe(Effect.catchTag('PlatformError', error => {
|
|
68
|
+
if (error.reason._tag !== 'NotFound') return Effect.fail(error);
|
|
69
|
+
const parent = path.dirname(filename);
|
|
70
|
+
if (parent === filename) return Effect.fail(error);
|
|
71
|
+
return Effect.gen(function* () {
|
|
72
|
+
const link = yield* fs.readLink(filename).pipe(Effect.asSome, Effect.catchReason('PlatformError', 'NotFound', () => Effect.succeedNone));
|
|
73
|
+
return Option.isSome(link) ? yield* canonicalPath(path.resolve(parent, link.value)) : path.join(yield* canonicalPath(parent), path.basename(filename));
|
|
74
|
+
});
|
|
75
|
+
}));
|
|
76
|
+
if (options.dbPath !== ':memory:' && options.cachePath !== ':memory:') {
|
|
77
|
+
const [snapshotFile, cacheFile] = yield* Effect.all([canonicalPath(options.dbPath), canonicalPath(options.cachePath)]);
|
|
78
|
+
const [snapshotInfo, cacheInfo] = yield* Effect.forEach([snapshotFile, cacheFile], filename => fs.stat(filename).pipe(Effect.asSome, Effect.catchReason('PlatformError', 'NotFound', () => Effect.succeedNone)));
|
|
79
|
+
const sameInode = Option.isSome(snapshotInfo) && Option.isSome(cacheInfo) && snapshotInfo.value.dev === cacheInfo.value.dev && Option.isSome(snapshotInfo.value.ino) && Option.isSome(cacheInfo.value.ino) && snapshotInfo.value.ino.value === cacheInfo.value.ino.value;
|
|
80
|
+
if (snapshotFile === cacheFile || sameInode) {
|
|
81
|
+
return yield* new RepositoryPathConflict({
|
|
82
|
+
snapshotPath: options.dbPath,
|
|
83
|
+
cachePath: options.cachePath
|
|
84
|
+
});
|
|
107
85
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
86
|
+
}
|
|
87
|
+
let initializeCache = options.cachePath === ':memory:';
|
|
88
|
+
if (options.cachePath !== ':memory:') {
|
|
89
|
+
yield* fs.makeDirectory(path.dirname(options.cachePath), {
|
|
90
|
+
recursive: true
|
|
91
|
+
});
|
|
92
|
+
initializeCache = !(yield* fs.exists(options.cachePath));
|
|
93
|
+
if (!initializeCache) {
|
|
94
|
+
const compatible = yield* Effect.scoped(Effect.gen(function* () {
|
|
95
|
+
const sql = yield* openClient({
|
|
96
|
+
filename: options.cachePath,
|
|
97
|
+
readonly: true,
|
|
98
|
+
disableWAL: true
|
|
99
|
+
});
|
|
100
|
+
yield* checkFormat(sql, 'cache', options.cachePath);
|
|
101
|
+
return true;
|
|
102
|
+
})).pipe(Effect.catchTag('RepositoryFormatError', () => Effect.succeed(false)));
|
|
103
|
+
if (!compatible) {
|
|
104
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
105
|
+
yield* fs.remove(`${options.cachePath}${suffix}`, {
|
|
106
|
+
force: true
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
initializeCache = true;
|
|
112
110
|
}
|
|
113
|
-
yield* sql`
|
|
114
|
-
ALTER TABLE ${sql(tables.testRunResults)}
|
|
115
|
-
ADD COLUMN ${sql(column)} INTEGER NOT NULL DEFAULT 0;
|
|
116
|
-
`;
|
|
117
|
-
yield* sql`
|
|
118
|
-
UPDATE ${sql(tables.testRunResults)} AS runres
|
|
119
|
-
SET ${sql(column)} = (
|
|
120
|
-
SELECT ${sql(column)} FROM ${sql(tables.testResults)} res
|
|
121
|
-
WHERE res.id = runres.testResult
|
|
122
|
-
);
|
|
123
|
-
`;
|
|
124
111
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
112
|
+
}
|
|
113
|
+
const cache = yield* openClient({
|
|
114
|
+
filename: options.cachePath
|
|
115
|
+
});
|
|
116
|
+
yield* cache`PRAGMA foreign_keys = ON;`;
|
|
117
|
+
if (initializeCache) yield* initializeSchema(cache, 'cache', options.cachePath);
|
|
118
|
+
const getMemorySnapshot = snapshotLock.withPermit(Effect.gen(function* () {
|
|
119
|
+
const existing = yield* Ref.get(snapshotMemory);
|
|
120
|
+
if (Option.isSome(existing)) return existing.value;
|
|
121
|
+
const sql = yield* Scope.provide(openClient({
|
|
122
|
+
filename: ':memory:',
|
|
123
|
+
disableWAL: true
|
|
124
|
+
}), lifetime);
|
|
125
|
+
yield* sql`PRAGMA foreign_keys = ON;`;
|
|
126
|
+
yield* initializeSchema(sql, 'snapshot', ':memory:');
|
|
127
|
+
yield* Ref.set(snapshotMemory, Option.some(sql));
|
|
128
|
+
return sql;
|
|
131
129
|
}));
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
timeMillis: _.timeMillis
|
|
154
|
-
}));
|
|
155
|
-
for (let i = 0; i < junction.length; i += INSERT_BATCH_SIZE) {
|
|
156
|
-
const chunk = junction.slice(i, i + INSERT_BATCH_SIZE);
|
|
157
|
-
yield* sql`
|
|
158
|
-
INSERT INTO ${sql(tables.testRunResults)}
|
|
159
|
-
${sql.insert(chunk)}
|
|
160
|
-
ON CONFLICT(testRun, testResult) DO UPDATE SET
|
|
161
|
-
ordering = excluded.ordering,
|
|
162
|
-
resultOrdering = excluded.resultOrdering,
|
|
163
|
-
timeMillis = excluded.timeMillis;
|
|
164
|
-
`.pipe(Effect.mapError(RepositoryWriteError.from('insertTestResults')));
|
|
130
|
+
const snapshotReader = Effect.gen(function* () {
|
|
131
|
+
if (options.dbPath === ':memory:') return yield* Ref.get(snapshotMemory);
|
|
132
|
+
if (!(yield* fs.exists(options.dbPath))) return Option.none();
|
|
133
|
+
const sql = yield* openClient({
|
|
134
|
+
filename: options.dbPath,
|
|
135
|
+
readonly: true,
|
|
136
|
+
disableWAL: true
|
|
137
|
+
});
|
|
138
|
+
yield* checkFormat(sql, 'snapshot', options.dbPath);
|
|
139
|
+
return Option.some(sql);
|
|
140
|
+
});
|
|
141
|
+
const withSnapshot = (empty, use) => Effect.scoped(Effect.gen(function* () {
|
|
142
|
+
const sql = yield* snapshotReader;
|
|
143
|
+
return Option.isSome(sql) ? yield* use(sql.value) : empty;
|
|
144
|
+
}));
|
|
145
|
+
const cacheReader = Effect.gen(function* () {
|
|
146
|
+
if (options.cachePath === ':memory:') {
|
|
147
|
+
// A memory store has one connection. Block writes to attempts while
|
|
148
|
+
// readers page through them; temporary statistics remain writable.
|
|
149
|
+
yield* TxReentrantLock.readLock(memoryLock);
|
|
150
|
+
return cache;
|
|
165
151
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const result = schemas?.result ?? Schema.Unknown;
|
|
171
|
-
const expected = schemas?.expected ?? Schema.Unknown;
|
|
172
|
-
return Schema.Struct({
|
|
173
|
-
id: Schema.String,
|
|
174
|
-
hashTestCase: Schema.String,
|
|
175
|
-
key: Schema.String,
|
|
176
|
-
ordering: Schema.Int,
|
|
177
|
-
resultOrdering: Schema.Int,
|
|
178
|
-
input: Schema.fromJsonString(input),
|
|
179
|
-
result: Schema.fromJsonString(result),
|
|
180
|
-
expected: Schema.fromJsonString(expected),
|
|
181
|
-
label: LabelSchema,
|
|
182
|
-
tags: split(','),
|
|
183
|
-
timeMillis: Schema.Finite
|
|
152
|
+
const sql = yield* openClient({
|
|
153
|
+
filename: options.cachePath,
|
|
154
|
+
readonly: true,
|
|
155
|
+
disableWAL: true
|
|
184
156
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// The SQLite driver returns rows in one batch. Decode that batch once
|
|
190
|
-
// before exposing it as a stream.
|
|
191
|
-
return Stream.unwrap(Effect.gen(function* () {
|
|
192
|
-
const rows = yield* sql`
|
|
193
|
-
SELECT res.id, res.hashTestCase, res.key,
|
|
194
|
-
runres.ordering, runres.resultOrdering,
|
|
195
|
-
res.input, res.result, res.expected, res.label, res.tags,
|
|
196
|
-
runres.timeMillis
|
|
197
|
-
FROM ${sql(tables.testResults)} res
|
|
198
|
-
JOIN ${sql(tables.testRunResults)} runres ON res.id = runres.testResult
|
|
199
|
-
JOIN ${sql(tables.testRuns)} runs ON runs.id = runres.testRun
|
|
200
|
-
WHERE runs.id = ${testRun.id}
|
|
201
|
-
ORDER BY runres.ordering ASC, runres.resultOrdering ASC;
|
|
202
|
-
`.pipe(Effect.mapError(RepositoryQueryError.fromSql('getTestResultsStream')));
|
|
203
|
-
const decoded = yield* Schema.decodeUnknownEffect(RowsSchema)(rows).pipe(Effect.mapError(RepositoryDecodeError.from('getTestResultsStream')));
|
|
204
|
-
return Stream.fromIterable(decoded);
|
|
205
|
-
}));
|
|
206
|
-
};
|
|
207
|
-
const hasResults = Effect.fnUntraced(function* (testRun) {
|
|
208
|
-
const b = yield* sql`
|
|
209
|
-
SELECT COUNT(*) count FROM ${sql(tables.testRunResults)}
|
|
210
|
-
WHERE testRun = ${testRun.id};
|
|
211
|
-
`.pipe(Effect.mapError(RepositoryQueryError.fromSql('hasResults')));
|
|
212
|
-
const r = yield* Schema.decodeUnknownEffect(CountSchema)(b).pipe(Effect.mapError(RepositoryDecodeError.from('hasResults')));
|
|
213
|
-
return r > 0;
|
|
157
|
+
// Keep the selected rows stable until the scoped connection closes,
|
|
158
|
+
// including when reading is interrupted.
|
|
159
|
+
yield* sql`BEGIN;`;
|
|
160
|
+
return sql;
|
|
214
161
|
});
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const p = yield* Schema.decodeUnknownEffect(TestRunsReadSchema)(r).pipe(Effect.mapError(RepositoryDecodeError.from('getPreviousTestRun')));
|
|
223
|
-
return A.head(p);
|
|
162
|
+
const writeCache = effect => options.cachePath === ':memory:' ? TxReentrantLock.withWriteLock(memoryLock, cache.withTransaction(effect)) : cache.withTransaction(effect);
|
|
163
|
+
const readRun = Effect.fnUntraced(function* (sql, run) {
|
|
164
|
+
return A.head(yield* decodeRuns(yield* sql`
|
|
165
|
+
SELECT *, ${run.store} AS store FROM ${sql(tables.testRuns)}
|
|
166
|
+
WHERE id = ${run.id}
|
|
167
|
+
AND ${run.store === 'cache' ? sql`snapshotIdentity = ${options.snapshotIdentity}` : sql`1 = 1`};
|
|
168
|
+
`));
|
|
224
169
|
});
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
170
|
+
const getTestRun = run => run.store === 'cache' ? readRun(cache, run) : withSnapshot(Option.none(), sql => readRun(sql, run));
|
|
171
|
+
const requireRun = Effect.fnUntraced(function* (sql, run) {
|
|
172
|
+
const found = yield* readRun(sql, run);
|
|
173
|
+
if (Option.isNone(found) || found.value.name !== run.name || found.value.variant !== run.variant) {
|
|
174
|
+
return yield* new TestRunNotFound({
|
|
175
|
+
testRunId: run.id
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return found.value;
|
|
230
179
|
});
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
180
|
+
const writeRows = Effect.fnUntraced(function* (sql, runId, rows) {
|
|
181
|
+
if (rows.length === 0) return;
|
|
182
|
+
yield* sql`
|
|
183
|
+
INSERT INTO ${sql(tables.testResults)} ${sql.insert(rows.map(row => ({
|
|
184
|
+
id: row.id,
|
|
185
|
+
hashTestCase: row.hashTestCase,
|
|
186
|
+
key: row.key,
|
|
187
|
+
input: row.input,
|
|
188
|
+
result: row.result,
|
|
189
|
+
expected: row.expected,
|
|
190
|
+
label: row.label
|
|
191
|
+
})))} ON CONFLICT(id) DO NOTHING;
|
|
192
|
+
`;
|
|
193
|
+
yield* sql`
|
|
194
|
+
INSERT INTO ${sql(tables.testRunResults)} ${sql.insert(rows.map(row => ({
|
|
195
|
+
testRun: runId,
|
|
196
|
+
testResult: row.id,
|
|
197
|
+
ordering: row.ordering,
|
|
198
|
+
resultOrdering: row.resultOrdering,
|
|
199
|
+
timeMillis: row.timeMillis,
|
|
200
|
+
tags: row.tags
|
|
201
|
+
})))} ON CONFLICT(testRun, testResult) DO UPDATE SET
|
|
202
|
+
ordering = excluded.ordering, resultOrdering = excluded.resultOrdering,
|
|
203
|
+
timeMillis = excluded.timeMillis, tags = excluded.tags;
|
|
204
|
+
`;
|
|
236
205
|
});
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
206
|
+
const insertTestResults = Effect.fnUntraced(function* (results, run, schemas) {
|
|
207
|
+
if (run.store !== 'cache') {
|
|
208
|
+
return yield* Effect.die(new Error('Only cached Running attempts accept result writes.'));
|
|
209
|
+
}
|
|
210
|
+
const codec = makeResultCodec(schemas, run.name);
|
|
211
|
+
// Fifty rows keep every statement below SQLite's 999-parameter minimum.
|
|
212
|
+
for (let offset = 0; offset < Math.max(1, results.length); offset += 50) {
|
|
213
|
+
const encoded = yield* Effect.forEach(results.slice(offset, offset + 50), Effect.fnUntraced(function* (result) {
|
|
214
|
+
const encoded = yield* codec.encode(result);
|
|
215
|
+
return yield* encodeResult({
|
|
216
|
+
...encoded,
|
|
217
|
+
input: stringifyJson(encoded.input),
|
|
218
|
+
result: stringifyJson(encoded.result),
|
|
219
|
+
expected: stringifyJson(encoded.expected)
|
|
220
|
+
});
|
|
221
|
+
}));
|
|
222
|
+
yield* writeCache(Effect.gen(function* () {
|
|
223
|
+
const saved = yield* requireRun(cache, run);
|
|
224
|
+
if (saved.state !== 'Running') {
|
|
225
|
+
return yield* Effect.die(new Error('Only Running test runs accept result writes.'));
|
|
226
|
+
}
|
|
227
|
+
yield* writeRows(cache, run.id, encoded);
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
242
230
|
});
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
231
|
+
const readResultPage = (sql, runId, after, matching, limit = 100) => {
|
|
232
|
+
const filter = matching === undefined ? sql`1 = 1` : matching.length === 0 ? sql`1 = 0` : sql.or(matching.map(key => sql`res.hashTestCase = ${key.hashTestCase} AND res.key = ${key.key}`));
|
|
233
|
+
// CROSS JOIN makes matching batches use the case/key index first.
|
|
234
|
+
const from = matching === undefined ? sql`${sql(tables.testRunResults)} runres JOIN ${sql(tables.testResults)} res ON res.id = runres.testResult` : sql`${sql(tables.testResults)} res CROSS JOIN ${sql(tables.testRunResults)} runres ON res.id = runres.testResult`;
|
|
235
|
+
return sql`
|
|
236
|
+
SELECT res.id, res.hashTestCase, res.key, runres.ordering, runres.resultOrdering,
|
|
237
|
+
res.input, res.result, res.expected, res.label, runres.tags, runres.timeMillis
|
|
238
|
+
FROM ${from}
|
|
239
|
+
WHERE runres.testRun = ${runId} AND ${filter}
|
|
240
|
+
AND ${after === undefined ? sql`1 = 1` : sql`
|
|
241
|
+
(runres.ordering, runres.resultOrdering, runres.testResult) >
|
|
242
|
+
(${after.ordering}, ${after.resultOrdering}, ${after.id})`}
|
|
243
|
+
ORDER BY runres.ordering, runres.resultOrdering, runres.testResult LIMIT ${limit};
|
|
244
|
+
`;
|
|
245
|
+
};
|
|
246
|
+
const getTestResultsStream = (run, schemas, matching) => {
|
|
247
|
+
if (matching !== undefined && matching.length > 50) return Stream.die(new Error('Match at most 50 case/key pairs per query.'));
|
|
248
|
+
const codec = makeResultCodec(schemas, run.name);
|
|
249
|
+
return Stream.unwrap(Effect.gen(function* () {
|
|
250
|
+
const selected = run.store === 'cache' ? Option.some(yield* cacheReader) : yield* snapshotReader;
|
|
251
|
+
if (Option.isNone(selected)) return yield* new TestRunNotFound({
|
|
252
|
+
testRunId: run.id
|
|
253
|
+
});
|
|
254
|
+
const sql = selected.value;
|
|
255
|
+
yield* requireRun(sql, run);
|
|
256
|
+
return Stream.paginate(undefined, Effect.fnUntraced(function* (after) {
|
|
257
|
+
const rows = yield* decodeRows(yield* readResultPage(sql, run.id, after, matching));
|
|
258
|
+
const decoded = yield* Effect.forEach(rows, codec.decode);
|
|
259
|
+
return [decoded, rows.length < 100 ? Option.none() : Option.some(rows[rows.length - 1])];
|
|
260
|
+
}));
|
|
261
|
+
}));
|
|
262
|
+
};
|
|
263
|
+
const getLatestTestRun = Effect.fnUntraced(function* ({
|
|
264
|
+
name,
|
|
265
|
+
variant
|
|
266
|
+
}) {
|
|
267
|
+
return A.head(yield* decodeRuns(yield* cache`
|
|
268
|
+
SELECT *, 'cache' AS store FROM ${cache(tables.testRuns)}
|
|
269
|
+
WHERE snapshotIdentity = ${options.snapshotIdentity} AND name = ${name} AND variant = ${variant}
|
|
270
|
+
ORDER BY id DESC LIMIT 1;
|
|
271
|
+
`));
|
|
253
272
|
});
|
|
254
|
-
const
|
|
255
|
-
const currentRaw = yield* sql`
|
|
256
|
-
SELECT *
|
|
257
|
-
FROM ${sql(tables.testRuns)}
|
|
258
|
-
WHERE hash IS NULL AND name = ${name}
|
|
259
|
-
ORDER BY id DESC
|
|
260
|
-
LIMIT 1;
|
|
261
|
-
`.pipe(Effect.mapError(RepositoryQueryError.fromSql('getOrCreateCurrentTestRun')));
|
|
262
|
-
const current = yield* Schema.decodeUnknownEffect(TestRunsReadSchema)(currentRaw).pipe(Effect.mapError(RepositoryDecodeError.from('getOrCreateCurrentTestRun')));
|
|
263
|
-
const existing = A.head(current);
|
|
264
|
-
if (Option.isSome(existing)) {
|
|
265
|
-
return existing.value;
|
|
266
|
-
}
|
|
267
|
-
const insertedRaw = yield* sql`
|
|
268
|
-
INSERT INTO ${sql(tables.testRuns)}
|
|
269
|
-
${sql.insert({
|
|
270
|
-
name
|
|
271
|
-
})}
|
|
272
|
-
RETURNING *;
|
|
273
|
-
`.pipe(Effect.mapError(RepositoryWriteError.from('getOrCreateCurrentTestRun')));
|
|
274
|
-
const inserted = yield* Schema.decodeUnknownEffect(TestRunsReadSchema)(insertedRaw).pipe(Effect.mapError(RepositoryDecodeError.from('getOrCreateCurrentTestRun')));
|
|
275
|
-
return yield* Effect.fromOption(A.head(inserted)).pipe(Effect.mapError(() => new TestRunNotCreated({
|
|
276
|
-
testSuiteName: name
|
|
277
|
-
})));
|
|
278
|
-
})).pipe(Effect.catchTag('SqlError', e => Effect.fail(RepositoryQueryError.fromSql('getOrCreateCurrentTestRun')(e))));
|
|
279
|
-
const clearStale = ({
|
|
273
|
+
const getBaselineTestRun = ({
|
|
280
274
|
name,
|
|
281
|
-
|
|
282
|
-
}) =>
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
275
|
+
variant
|
|
276
|
+
}) => withSnapshot(Option.none(), Effect.fnUntraced(function* (sql) {
|
|
277
|
+
return A.head(yield* decodeRuns(yield* sql`
|
|
278
|
+
SELECT run.*, 'snapshot' AS store FROM ${sql(tables.testBaselines)} baseline
|
|
279
|
+
JOIN ${sql(tables.testRuns)} run ON run.id = baseline.testRun
|
|
280
|
+
WHERE baseline.name = ${name} AND baseline.variant = ${variant};
|
|
281
|
+
`));
|
|
282
|
+
}));
|
|
283
|
+
const createTestRun = Effect.fnUntraced(function* (input) {
|
|
284
|
+
const executionId = yield* crypto.randomUUIDv4();
|
|
285
|
+
return yield* writeCache(Effect.gen(function* () {
|
|
286
|
+
yield* cache`
|
|
287
|
+
DELETE FROM ${cache(tables.testRuns)}
|
|
288
|
+
WHERE snapshotIdentity = ${options.snapshotIdentity} AND name = ${input.name} AND variant = ${input.variant};
|
|
289
|
+
`;
|
|
290
|
+
yield* cache`
|
|
291
|
+
DELETE FROM ${cache(tables.testResults)} WHERE NOT EXISTS (
|
|
292
|
+
SELECT 1 FROM ${cache(tables.testRunResults)} links WHERE links.testResult = ${cache(tables.testResults)}.id
|
|
293
|
+
);
|
|
294
|
+
`;
|
|
295
|
+
const inserted = yield* decodeRuns(yield* cache`
|
|
296
|
+
INSERT INTO ${cache(tables.testRuns)} ${cache.insert({
|
|
297
|
+
name: input.name,
|
|
298
|
+
variant: input.variant,
|
|
299
|
+
executionId,
|
|
300
|
+
snapshotIdentity: options.snapshotIdentity,
|
|
301
|
+
state: 'Running'
|
|
302
|
+
})}
|
|
303
|
+
RETURNING *, 'cache' AS store;
|
|
304
|
+
`);
|
|
305
|
+
return yield* Effect.fromOption(A.head(inserted)).pipe(Effect.mapError(() => new TestRunNotCreated({
|
|
306
|
+
testSuiteName: input.name
|
|
307
|
+
})));
|
|
308
|
+
}));
|
|
309
|
+
});
|
|
310
|
+
const finishTestRun = (run, state) => writeCache(Effect.gen(function* () {
|
|
311
|
+
const updated = yield* decodeRuns(yield* cache`
|
|
312
|
+
UPDATE ${cache(tables.testRuns)} SET state = ${state}
|
|
313
|
+
WHERE id = ${run.id} AND snapshotIdentity = ${options.snapshotIdentity} AND state = 'Running'
|
|
314
|
+
RETURNING *, 'cache' AS store;
|
|
315
|
+
`);
|
|
316
|
+
if (updated.length === 0) return yield* new TestRunNotFound({
|
|
317
|
+
testRunId: run.id
|
|
318
|
+
});
|
|
319
|
+
}));
|
|
320
|
+
const withTestRun = (input, execute) => Effect.acquireUseRelease(createTestRun(input), run => Effect.suspend(() => execute(run)).pipe(Effect.map(value => ({
|
|
321
|
+
run,
|
|
322
|
+
value
|
|
323
|
+
}))), (run, exit) => finishTestRun(run, Exit.isSuccess(exit) ? 'Complete' : 'Failed')).pipe(Effect.map(({
|
|
324
|
+
run,
|
|
325
|
+
value
|
|
326
|
+
}) => ({
|
|
327
|
+
run: TestRun.make({
|
|
328
|
+
...run,
|
|
329
|
+
state: 'Complete'
|
|
330
|
+
}),
|
|
331
|
+
value
|
|
332
|
+
})));
|
|
333
|
+
const snapshotWriter = Effect.gen(function* () {
|
|
334
|
+
if (options.dbPath === ':memory:') return yield* getMemorySnapshot;
|
|
335
|
+
const exists = yield* fs.exists(options.dbPath);
|
|
336
|
+
if (exists) {
|
|
337
|
+
yield* Effect.scoped(Effect.gen(function* () {
|
|
338
|
+
const sql = yield* openClient({
|
|
339
|
+
filename: options.dbPath,
|
|
340
|
+
readonly: true,
|
|
341
|
+
disableWAL: true
|
|
342
|
+
});
|
|
343
|
+
yield* checkFormat(sql, 'snapshot', options.dbPath);
|
|
344
|
+
}));
|
|
345
|
+
} else {
|
|
346
|
+
yield* fs.makeDirectory(path.dirname(options.dbPath), {
|
|
347
|
+
recursive: true
|
|
348
|
+
});
|
|
293
349
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
yield* sql`
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}) => sql.withTransaction(Effect.gen(function* () {
|
|
314
|
-
const currentRaw = yield* sql`
|
|
315
|
-
SELECT *
|
|
316
|
-
FROM ${sql(tables.testRuns)}
|
|
317
|
-
WHERE hash IS NULL AND name = ${name}
|
|
318
|
-
ORDER BY id DESC
|
|
319
|
-
LIMIT 1;
|
|
320
|
-
`.pipe(Effect.mapError(RepositoryQueryError.fromSql('clearUncommitedTestResults')));
|
|
321
|
-
const current = yield* Schema.decodeUnknownEffect(TestRunsReadSchema)(currentRaw).pipe(Effect.mapError(RepositoryDecodeError.from('clearUncommitedTestResults')));
|
|
322
|
-
const currentTestRun = A.head(current);
|
|
323
|
-
if (Option.isNone(currentTestRun)) {
|
|
324
|
-
return;
|
|
350
|
+
const sql = yield* openClient({
|
|
351
|
+
filename: options.dbPath,
|
|
352
|
+
disableWAL: true
|
|
353
|
+
});
|
|
354
|
+
if (exists) yield* checkFormat(sql, 'snapshot', options.dbPath);else yield* initializeSchema(sql, 'snapshot', options.dbPath);
|
|
355
|
+
yield* sql`PRAGMA journal_mode = DELETE;`;
|
|
356
|
+
yield* sql`PRAGMA foreign_keys = ON;`;
|
|
357
|
+
return sql;
|
|
358
|
+
});
|
|
359
|
+
const commitTestRuns = runs => Effect.scoped(Effect.gen(function* () {
|
|
360
|
+
if (runs.length === 0) return [];
|
|
361
|
+
const selectedVariants = new Set();
|
|
362
|
+
for (const run of runs) {
|
|
363
|
+
const key = JSON.stringify([run.name, run.variant]);
|
|
364
|
+
if (selectedVariants.has(key)) return yield* new TestRunDuplicate({
|
|
365
|
+
testSuiteName: run.name,
|
|
366
|
+
variant: run.variant
|
|
367
|
+
});
|
|
368
|
+
selectedVariants.add(key);
|
|
325
369
|
}
|
|
326
|
-
yield*
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
370
|
+
const reader = runs.some(run => run.store === 'cache') ? yield* cacheReader : undefined;
|
|
371
|
+
// Check every selected run before opening a writer, so a missing or
|
|
372
|
+
// incomplete attempt cannot create a snapshot file.
|
|
373
|
+
const selected = yield* Effect.forEach(runs, run => Effect.gen(function* () {
|
|
374
|
+
const saved = run.store === 'cache' && reader !== undefined ? yield* requireRun(reader, run) : yield* getTestRun(run).pipe(Effect.flatMap(Option.match({
|
|
375
|
+
onNone: () => Effect.fail(new TestRunNotFound({
|
|
376
|
+
testRunId: run.id
|
|
377
|
+
})),
|
|
378
|
+
onSome: Effect.succeed
|
|
379
|
+
})));
|
|
380
|
+
if (saved.name !== run.name || saved.variant !== run.variant) return yield* new TestRunNotFound({
|
|
381
|
+
testRunId: run.id
|
|
382
|
+
});
|
|
383
|
+
if (saved.state !== 'Complete') return yield* new TestRunIncomplete({
|
|
384
|
+
testSuiteName: saved.name,
|
|
385
|
+
testRunId: saved.id,
|
|
386
|
+
variant: saved.variant
|
|
387
|
+
});
|
|
388
|
+
if (saved.store === 'snapshot' || reader === undefined) return {
|
|
389
|
+
run: saved,
|
|
390
|
+
executionId: undefined
|
|
391
|
+
};
|
|
392
|
+
const stored = yield* decodeStoredRuns(yield* reader`
|
|
393
|
+
SELECT *, 'cache' AS store FROM ${reader(tables.testRuns)}
|
|
394
|
+
WHERE id = ${saved.id} AND snapshotIdentity = ${options.snapshotIdentity};
|
|
395
|
+
`);
|
|
396
|
+
return {
|
|
397
|
+
run: saved,
|
|
398
|
+
executionId: stored[0].executionId
|
|
399
|
+
};
|
|
400
|
+
}));
|
|
401
|
+
const sql = yield* snapshotWriter;
|
|
402
|
+
return yield* sql.withTransaction(Effect.forEach(selected, ({
|
|
403
|
+
run,
|
|
404
|
+
executionId
|
|
405
|
+
}) => Effect.gen(function* () {
|
|
406
|
+
let snapshot;
|
|
407
|
+
if (run.store === 'snapshot') {
|
|
408
|
+
snapshot = yield* requireRun(sql, run);
|
|
409
|
+
} else {
|
|
410
|
+
const existing = yield* decodeRuns(yield* sql`
|
|
411
|
+
SELECT *, 'snapshot' AS store FROM ${sql(tables.testRuns)} WHERE executionId = ${executionId};
|
|
412
|
+
`);
|
|
413
|
+
if (existing.length > 0) {
|
|
414
|
+
snapshot = existing[0];
|
|
415
|
+
} else {
|
|
416
|
+
const inserted = yield* decodeRuns(yield* sql`
|
|
417
|
+
INSERT INTO ${sql(tables.testRuns)} ${sql.insert({
|
|
418
|
+
name: run.name,
|
|
419
|
+
variant: run.variant,
|
|
420
|
+
executionId,
|
|
421
|
+
snapshotIdentity: options.snapshotIdentity,
|
|
422
|
+
state: 'Complete'
|
|
423
|
+
})} RETURNING *, 'snapshot' AS store;
|
|
424
|
+
`);
|
|
425
|
+
snapshot = inserted[0];
|
|
426
|
+
let after;
|
|
427
|
+
if (reader !== undefined) {
|
|
428
|
+
while (true) {
|
|
429
|
+
const rows = yield* decodeRawRows(yield* readResultPage(reader, run.id, after, undefined, 50));
|
|
430
|
+
yield* writeRows(sql, snapshot.id, rows);
|
|
431
|
+
if (rows.length < 50) break;
|
|
432
|
+
after = rows[rows.length - 1];
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
yield* sql`
|
|
438
|
+
INSERT INTO ${sql(tables.testBaselines)} ${sql.insert({
|
|
439
|
+
name: snapshot.name,
|
|
440
|
+
variant: snapshot.variant,
|
|
441
|
+
testRun: snapshot.id
|
|
442
|
+
})}
|
|
443
|
+
ON CONFLICT(name, variant) DO UPDATE SET testRun = excluded.testRun;
|
|
444
|
+
`;
|
|
445
|
+
return snapshot;
|
|
446
|
+
})));
|
|
447
|
+
}));
|
|
448
|
+
const listTestRuns = Effect.fnUntraced(function* ({
|
|
338
449
|
name,
|
|
339
|
-
|
|
450
|
+
variant,
|
|
451
|
+
before,
|
|
452
|
+
limit
|
|
340
453
|
}) {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
`.pipe(Effect.mapError(RepositoryWriteError.from('commitCurrentTestRun')));
|
|
362
|
-
yield* sql`
|
|
363
|
-
INSERT INTO ${sql(tables.testRuns)}
|
|
364
|
-
${sql.insert({
|
|
365
|
-
name
|
|
366
|
-
})};
|
|
367
|
-
`.pipe(Effect.mapError(RepositoryWriteError.from('commitCurrentTestRun')));
|
|
368
|
-
yield* Console.log('Commited test run.');
|
|
454
|
+
const parsedLimit = yield* decodeRunLimit(limit);
|
|
455
|
+
return yield* withSnapshot([], Effect.fnUntraced(function* (sql) {
|
|
456
|
+
const rows = yield* decodeListedRuns(yield* sql`
|
|
457
|
+
SELECT run.*, 'snapshot' AS store, EXISTS (
|
|
458
|
+
SELECT 1 FROM ${sql(tables.testBaselines)} baseline
|
|
459
|
+
WHERE baseline.name = run.name AND baseline.variant = run.variant AND baseline.testRun = run.id
|
|
460
|
+
) AS isBaseline FROM ${sql(tables.testRuns)} run
|
|
461
|
+
WHERE run.name = ${name}
|
|
462
|
+
AND ${variant === undefined ? sql`1 = 1` : sql`run.variant = ${variant}`}
|
|
463
|
+
AND ${before === undefined ? sql`1 = 1` : sql`run.id < ${before}`}
|
|
464
|
+
ORDER BY run.id DESC LIMIT ${parsedLimit};
|
|
465
|
+
`);
|
|
466
|
+
return rows.map(({
|
|
467
|
+
isBaseline,
|
|
468
|
+
...run
|
|
469
|
+
}) => ({
|
|
470
|
+
...run,
|
|
471
|
+
isBaseline: isBaseline === 1
|
|
472
|
+
}));
|
|
473
|
+
}));
|
|
369
474
|
});
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
475
|
+
const listVariantBaselines = name => withSnapshot([], Effect.fnUntraced(function* (sql) {
|
|
476
|
+
const rows = yield* decodeVariantBaselines(yield* sql`
|
|
477
|
+
SELECT variants.variant, run.id, run.name, run.state FROM (
|
|
478
|
+
SELECT DISTINCT variant FROM ${sql(tables.testRuns)} WHERE name = ${name}
|
|
479
|
+
) variants LEFT JOIN ${sql(tables.testBaselines)} baseline
|
|
480
|
+
ON baseline.name = ${name} AND baseline.variant = variants.variant
|
|
481
|
+
LEFT JOIN ${sql(tables.testRuns)} run ON run.id = baseline.testRun ORDER BY variants.variant;
|
|
482
|
+
`);
|
|
483
|
+
return rows.map(({
|
|
484
|
+
variant,
|
|
485
|
+
id,
|
|
486
|
+
name
|
|
487
|
+
}) => ({
|
|
488
|
+
variant,
|
|
489
|
+
baseline: id === null || name === null ? Option.none() : Option.some(TestRun.make({
|
|
490
|
+
id,
|
|
491
|
+
name,
|
|
492
|
+
variant,
|
|
493
|
+
store: 'snapshot',
|
|
494
|
+
state: 'Complete'
|
|
495
|
+
}))
|
|
496
|
+
}));
|
|
497
|
+
}));
|
|
498
|
+
const repository = {
|
|
499
|
+
commitTestRuns,
|
|
500
|
+
getTestRun,
|
|
501
|
+
getLatestTestRun,
|
|
502
|
+
getBaselineTestRun,
|
|
503
|
+
listTestRuns,
|
|
504
|
+
listVariantBaselines,
|
|
379
505
|
getTestResultsStream,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
insertTestResults,
|
|
383
|
-
|
|
506
|
+
withTestRun,
|
|
507
|
+
withStats: use => withStats(use).pipe(Effect.provideService(SqlClient.SqlClient, cache)),
|
|
508
|
+
insertTestResult: (result, run, schemas) => insertTestResults([result], run, schemas),
|
|
509
|
+
insertTestResults
|
|
510
|
+
};
|
|
511
|
+
return {
|
|
512
|
+
repository,
|
|
513
|
+
cache,
|
|
514
|
+
snapshot: getMemorySnapshot
|
|
384
515
|
};
|
|
385
|
-
return service;
|
|
386
516
|
});
|
|
387
517
|
//# sourceMappingURL=Test.repository.sqlite.js.map
|