@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.
Files changed (120) hide show
  1. package/README.md +143 -37
  2. package/dist/cjs/Classify.js +1 -2
  3. package/dist/cjs/Classify.js.map +1 -1
  4. package/dist/cjs/RepositoryError.js +42 -33
  5. package/dist/cjs/RepositoryError.js.map +1 -1
  6. package/dist/cjs/Show.js +18 -0
  7. package/dist/cjs/Show.js.map +1 -1
  8. package/dist/cjs/Test.js +40 -2
  9. package/dist/cjs/Test.js.map +1 -1
  10. package/dist/cjs/Test.repository.js +25 -22
  11. package/dist/cjs/Test.repository.js.map +1 -1
  12. package/dist/cjs/TestRun.js +28 -0
  13. package/dist/cjs/TestRun.js.map +1 -0
  14. package/dist/cjs/TestValueError.js +23 -0
  15. package/dist/cjs/TestValueError.js.map +1 -0
  16. package/dist/cjs/index.js +0 -7
  17. package/dist/cjs/index.js.map +1 -1
  18. package/dist/cjs/internal/Classify.js +1 -9
  19. package/dist/cjs/internal/Classify.js.map +1 -1
  20. package/dist/cjs/internal/Test.js +235 -95
  21. package/dist/cjs/internal/Test.js.map +1 -1
  22. package/dist/cjs/internal/Test.repository.sqlite.js +487 -356
  23. package/dist/cjs/internal/Test.repository.sqlite.js.map +1 -1
  24. package/dist/cjs/internal/Test.serialization.js +128 -0
  25. package/dist/cjs/internal/Test.serialization.js.map +1 -0
  26. package/dist/cjs/internal/common.js.map +1 -1
  27. package/dist/cjs/internal/stats.js +29 -1
  28. package/dist/cjs/internal/stats.js.map +1 -1
  29. package/dist/cjs/internal/summarize.js +143 -29
  30. package/dist/cjs/internal/summarize.js.map +1 -1
  31. package/dist/cjs/internal/test-repository-fixtures.js +31 -0
  32. package/dist/cjs/internal/test-repository-fixtures.js.map +1 -0
  33. package/dist/cjs/internal/test-repository-schema.js +98 -0
  34. package/dist/cjs/internal/test-repository-schema.js.map +1 -0
  35. package/dist/cjs/internal/test-result-stats.js +89 -0
  36. package/dist/cjs/internal/test-result-stats.js.map +1 -0
  37. package/dist/dts/Classify.d.ts +0 -1
  38. package/dist/dts/Classify.d.ts.map +1 -1
  39. package/dist/dts/RepositoryError.d.ts +47 -20
  40. package/dist/dts/RepositoryError.d.ts.map +1 -1
  41. package/dist/dts/Show.d.ts +5 -5
  42. package/dist/dts/Show.d.ts.map +1 -1
  43. package/dist/dts/Test.d.ts +136 -13
  44. package/dist/dts/Test.d.ts.map +1 -1
  45. package/dist/dts/Test.repository.d.ts +132 -9
  46. package/dist/dts/Test.repository.d.ts.map +1 -1
  47. package/dist/dts/TestRun.d.ts +22 -0
  48. package/dist/dts/TestRun.d.ts.map +1 -0
  49. package/dist/dts/TestValueError.d.ts +17 -0
  50. package/dist/dts/TestValueError.d.ts.map +1 -0
  51. package/dist/dts/index.d.ts +0 -1
  52. package/dist/dts/index.d.ts.map +1 -1
  53. package/dist/dts/internal/Classify.d.ts +0 -1
  54. package/dist/dts/internal/Classify.d.ts.map +1 -1
  55. package/dist/dts/internal/Test.d.ts +64 -30
  56. package/dist/dts/internal/Test.d.ts.map +1 -1
  57. package/dist/dts/internal/Test.repository.sqlite.d.ts +92 -11
  58. package/dist/dts/internal/Test.repository.sqlite.d.ts.map +1 -1
  59. package/dist/dts/internal/Test.serialization.d.ts +34 -0
  60. package/dist/dts/internal/Test.serialization.d.ts.map +1 -0
  61. package/dist/dts/internal/common.d.ts +1 -1
  62. package/dist/dts/internal/common.d.ts.map +1 -1
  63. package/dist/dts/internal/stats.d.ts +16 -1
  64. package/dist/dts/internal/stats.d.ts.map +1 -1
  65. package/dist/dts/internal/summarize.d.ts +39 -7
  66. package/dist/dts/internal/summarize.d.ts.map +1 -1
  67. package/dist/dts/internal/test-repository-fixtures.d.ts +33 -0
  68. package/dist/dts/internal/test-repository-fixtures.d.ts.map +1 -0
  69. package/dist/dts/internal/test-repository-schema.d.ts +16 -0
  70. package/dist/dts/internal/test-repository-schema.d.ts.map +1 -0
  71. package/dist/dts/internal/test-result-stats.d.ts +7 -0
  72. package/dist/dts/internal/test-result-stats.d.ts.map +1 -0
  73. package/dist/esm/Classify.js +0 -1
  74. package/dist/esm/Classify.js.map +1 -1
  75. package/dist/esm/RepositoryError.js +29 -29
  76. package/dist/esm/RepositoryError.js.map +1 -1
  77. package/dist/esm/Show.js +2 -2
  78. package/dist/esm/Show.js.map +1 -1
  79. package/dist/esm/Test.js +37 -1
  80. package/dist/esm/Test.js.map +1 -1
  81. package/dist/esm/Test.repository.js +26 -21
  82. package/dist/esm/Test.repository.js.map +1 -1
  83. package/dist/esm/TestRun.js +22 -0
  84. package/dist/esm/TestRun.js.map +1 -0
  85. package/dist/esm/TestValueError.js +16 -0
  86. package/dist/esm/TestValueError.js.map +1 -0
  87. package/dist/esm/index.js +0 -1
  88. package/dist/esm/index.js.map +1 -1
  89. package/dist/esm/internal/Classify.js +0 -7
  90. package/dist/esm/internal/Classify.js.map +1 -1
  91. package/dist/esm/internal/Test.js +231 -94
  92. package/dist/esm/internal/Test.js.map +1 -1
  93. package/dist/esm/internal/Test.repository.sqlite.js +486 -356
  94. package/dist/esm/internal/Test.repository.sqlite.js.map +1 -1
  95. package/dist/esm/internal/Test.serialization.js +119 -0
  96. package/dist/esm/internal/Test.serialization.js.map +1 -0
  97. package/dist/esm/internal/common.js.map +1 -1
  98. package/dist/esm/internal/stats.js +27 -0
  99. package/dist/esm/internal/stats.js.map +1 -1
  100. package/dist/esm/internal/summarize.js +143 -28
  101. package/dist/esm/internal/summarize.js.map +1 -1
  102. package/dist/esm/internal/test-repository-fixtures.js +25 -0
  103. package/dist/esm/internal/test-repository-fixtures.js.map +1 -0
  104. package/dist/esm/internal/test-repository-schema.js +92 -0
  105. package/dist/esm/internal/test-repository-schema.js.map +1 -0
  106. package/dist/esm/internal/test-result-stats.js +83 -0
  107. package/dist/esm/internal/test-result-stats.js.map +1 -0
  108. package/package.json +1 -1
  109. package/dist/cjs/Test.repository.types.js +0 -14
  110. package/dist/cjs/Test.repository.types.js.map +0 -1
  111. package/dist/cjs/internal/lib/Schema.js +0 -14
  112. package/dist/cjs/internal/lib/Schema.js.map +0 -1
  113. package/dist/dts/Test.repository.types.d.ts +0 -62
  114. package/dist/dts/Test.repository.types.d.ts.map +0 -1
  115. package/dist/dts/internal/lib/Schema.d.ts +0 -4
  116. package/dist/dts/internal/lib/Schema.d.ts.map +0 -1
  117. package/dist/esm/Test.repository.types.js +0 -7
  118. package/dist/esm/Test.repository.types.js.map +0 -1
  119. package/dist/esm/internal/lib/Schema.js +0 -7
  120. package/dist/esm/internal/lib/Schema.js.map +0 -1
@@ -1,387 +1,517 @@
1
- import * as SqlClient from 'effect/sql/SqlClient';
2
- import { Array as A, Console, Effect, Option, Schema, SchemaTransformation, Stream } from 'effect';
3
- import { TestRepository } from '../Test.repository.js';
4
- import { TestRun } from '../Test.repository.types.js';
5
- export { TestRepository };
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 { split } from './lib/Schema.js';
9
- export const tables = {
10
- testResults: 'test-results',
11
- testRuns: 'test-runs',
12
- testRunResults: 'test-run-results'
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
- ordering: Schema.Int,
19
- resultOrdering: Schema.Int,
20
- input: /*#__PURE__*/Schema.fromJsonString(Schema.Unknown),
21
- result: /*#__PURE__*/Schema.fromJsonString(Schema.Unknown),
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 TestResultsSchema = /*#__PURE__*/Schema.Array(TestResultWriteSchema);
28
- const CountSchema = /*#__PURE__*/Schema.Tuple([Schema.Struct({
29
- count: Schema.Int
30
- })]).pipe(/*#__PURE__*/Schema.decodeTo(Schema.Int, /*#__PURE__*/SchemaTransformation.transform({
31
- decode: ([{
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 TableColumnsSchema = /*#__PURE__*/Schema.Array(/*#__PURE__*/Schema.Struct({
47
- name: Schema.String
48
- }));
49
- export const makeTestRepository = /*#__PURE__*/Effect.gen(function* () {
50
- const sql = yield* SqlClient.SqlClient;
51
- yield* sql`PRAGMA auto_vacuum = FULL;`;
52
- yield* sql.withTransaction(Effect.gen(function* () {
53
- yield* sql`
54
- CREATE TABLE IF NOT EXISTS ${sql(tables.testRuns)}(
55
- id INTEGER PRIMARY KEY AUTOINCREMENT,
56
- name TEXT NOT NULL,
57
- hash TEXT
58
- );
59
- `;
60
- yield* sql`CREATE INDEX IF NOT EXISTS idx_name_hash ON ${sql(tables.testRuns)}(name, hash)`;
61
- // At most one "current" (hash IS NULL) row per suite name. Partial index
62
- // so committed rows are unaffected.
63
- yield* sql`
64
- CREATE UNIQUE INDEX IF NOT EXISTS idx_current_per_name
65
- ON ${sql(tables.testRuns)}(name)
66
- WHERE hash IS NULL;
67
- `;
68
- yield* sql`
69
- CREATE TABLE IF NOT EXISTS ${sql(tables.testResults)}(
70
- id TEXT PRIMARY KEY,
71
- hashTestCase TEXT NOT NULL,
72
- key TEXT NOT NULL DEFAULT 'result',
73
- ordering INTEGER NOT NULL,
74
- resultOrdering INTEGER NOT NULL DEFAULT 0,
75
- input TEXT,
76
- result TEXT,
77
- expected TEXT,
78
- label TEXT NOT NULL,
79
- tags TEXT NOT NULL,
80
- timeMillis INTEGER NOT NULL
81
- );
82
- `;
83
- yield* sql`
84
- CREATE TABLE IF NOT EXISTS ${sql(tables.testRunResults)}(
85
- testRun INTEGER NOT NULL,
86
- testResult TEXT NOT NULL,
87
- ordering INTEGER NOT NULL,
88
- resultOrdering INTEGER NOT NULL,
89
- timeMillis INTEGER NOT NULL,
90
- FOREIGN KEY (testRun) REFERENCES ${sql(tables.testRuns)}(id),
91
- FOREIGN KEY (testResult) REFERENCES ${sql(tables.testResults)}(id),
92
- UNIQUE (testRun, testResult)
93
- );
94
- `;
95
- const resultColumns = yield* Schema.decodeUnknownEffect(TableColumnsSchema)(yield* sql`PRAGMA table_info(${sql(tables.testResults)});`).pipe(Effect.mapError(RepositoryDecodeError.from('migrateTestResults')));
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
- const runResultColumns = yield* Schema.decodeUnknownEffect(TableColumnsSchema)(yield* sql`PRAGMA table_info(${sql(tables.testRunResults)});`).pipe(Effect.mapError(RepositoryDecodeError.from('migrateTestRunResults')));
109
- for (const column of ['ordering', 'resultOrdering', 'timeMillis']) {
110
- if (runResultColumns.some(existing => existing.name === column)) {
111
- continue;
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
- // Lookup by the junction's `testResult` column: the existing
126
- // UNIQUE(testRun, testResult) index is keyed on testRun first, so orphan
127
- // cleanup (`... NOT IN (SELECT testResult ...)`) and stream joins
128
- // (`res.id = runres.testResult`) would otherwise full-scan the junction.
129
- yield* sql`CREATE INDEX IF NOT EXISTS idx_trr_testresult ON ${sql(tables.testRunResults)}(testResult)`;
130
- yield* sql`CREATE INDEX IF NOT EXISTS idx_tr_hashtestcase ON ${sql(tables.testResults)}(hashTestCase)`;
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
- // With 11 result columns and 5 junction columns, batches of 50 rows stay below
133
- // SQLite's lowest default limit of 999 bound values per statement.
134
- const INSERT_BATCH_SIZE = 50;
135
- const insertTestResults = (testResults, testRun) => sql.withTransaction(Effect.gen(function* () {
136
- if (testResults.length === 0) return;
137
- const encoded = yield* Effect.forEach(testResults, _ => Schema.encodeEffect(TestResultWriteSchema)(_).pipe(Effect.mapError(RepositoryDecodeError.from('insertTestResults'))));
138
- for (let i = 0; i < encoded.length; i += INSERT_BATCH_SIZE) {
139
- const chunk = encoded.slice(i, i + INSERT_BATCH_SIZE);
140
- yield* sql`
141
- INSERT INTO ${sql(tables.testResults)}
142
- ${sql.insert(chunk)}
143
- ON CONFLICT(id) DO NOTHING;
144
- `.pipe(Effect.mapError(RepositoryWriteError.from('insertTestResults')));
145
- }
146
- // Junction rows reference the resolved testRun.id directly,
147
- // so no correlated subquery.
148
- const junction = encoded.map(_ => ({
149
- testRun: testRun.id,
150
- testResult: _.id,
151
- ordering: _.ordering,
152
- resultOrdering: _.resultOrdering,
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
- })).pipe(Effect.catchTag('SqlError', e => Effect.fail(RepositoryWriteError.from('insertTestResults')(e))));
167
- const insertTestResult = (testResult, testRun) => insertTestResults([testResult], testRun);
168
- const makeTestResultSchema = schemas => {
169
- const input = schemas?.input ?? Schema.Unknown;
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
- const getTestResultsStream = (testRun, schemas) => {
187
- const RowSchema = makeTestResultSchema(schemas);
188
- const RowsSchema = Schema.Array(RowSchema);
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 getPreviousTestRun = Effect.fnUntraced(function* (name) {
216
- const r = yield* sql`
217
- SELECT * FROM ${sql(tables.testRuns)}
218
- WHERE hash IS NOT NULL AND name = ${name}
219
- ORDER BY id DESC
220
- LIMIT 1;
221
- `.pipe(Effect.mapError(RepositoryQueryError.fromSql('getPreviousTestRun')));
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 getAllTestRuns = Effect.gen(function* () {
226
- const results = yield* sql`
227
- SELECT * from ${sql(tables.testRuns)};
228
- `.pipe(Effect.mapError(RepositoryQueryError.fromSql('getAllTestRuns')));
229
- return yield* Schema.decodeUnknownEffect(TestRunsReadSchema)(results).pipe(Effect.mapError(RepositoryDecodeError.from('getAllTestRuns')));
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 getAllTestRunResults = Effect.gen(function* () {
232
- const results = yield* sql`
233
- SELECT * from ${sql(tables.testRunResults)};
234
- `.pipe(Effect.mapError(RepositoryQueryError.fromSql('getAllTestRunResults')));
235
- return yield* Schema.decodeUnknownEffect(TestRunResultsSchema)(results).pipe(Effect.mapError(RepositoryDecodeError.from('getAllTestRunResults')));
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 getAllTestResults = Effect.gen(function* () {
238
- const results = yield* sql`
239
- SELECT * from ${sql(tables.testResults)};
240
- `.pipe(Effect.mapError(RepositoryQueryError.fromSql('getAllTestResults')));
241
- return yield* Schema.decodeUnknownEffect(TestResultsSchema)(results).pipe(Effect.mapError(RepositoryDecodeError.from('getAllTestResults')));
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 getLastTestRunHash = Effect.fnUntraced(function* (name) {
244
- const rows = yield* sql`
245
- SELECT *
246
- FROM ${sql(tables.testRuns)}
247
- WHERE hash IS NOT NULL AND name = ${name}
248
- ORDER BY id DESC
249
- LIMIT 1;
250
- `.pipe(Effect.mapError(RepositoryQueryError.fromSql('getLastTestRunHash')));
251
- const runs = yield* Schema.decodeUnknownEffect(TestRunsReadSchema)(rows).pipe(Effect.mapError(RepositoryDecodeError.from('getLastTestRunHash')));
252
- return A.head(runs).pipe(Option.flatMap(run => Option.fromNullishOr(run.hash)));
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 getOrCreateCurrentTestRun = name => sql.withTransaction(Effect.gen(function* () {
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
- keep = 1
282
- }) => sql.withTransaction(Effect.gen(function* () {
283
- const raw = yield* sql`
284
- SELECT *
285
- FROM ${sql(tables.testRuns)}
286
- WHERE hash IS NOT NULL AND name = ${name}
287
- ORDER BY id DESC
288
- LIMIT -1 OFFSET ${keep};
289
- `.pipe(Effect.mapError(RepositoryQueryError.fromSql('clearStale')));
290
- const staleTestRuns = yield* Schema.decodeUnknownEffect(TestRunsReadSchema)(raw).pipe(Effect.mapError(RepositoryDecodeError.from('clearStale')));
291
- if (staleTestRuns.length === 0) {
292
- return;
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 ids = staleTestRuns.map(r => r.id);
295
- yield* sql`
296
- DELETE FROM ${sql(tables.testRunResults)}
297
- WHERE testRun IN ${sql.in(ids)};
298
- `.pipe(Effect.mapError(RepositoryWriteError.from('clearStale')));
299
- yield* sql`
300
- DELETE FROM ${sql(tables.testRuns)}
301
- WHERE id IN ${sql.in(ids)};
302
- `.pipe(Effect.mapError(RepositoryWriteError.from('clearStale')));
303
- yield* sql`
304
- DELETE FROM ${sql(tables.testResults)}
305
- WHERE id NOT IN (
306
- SELECT DISTINCT testResult FROM ${sql(tables.testRunResults)}
307
- );
308
- `.pipe(Effect.mapError(RepositoryWriteError.from('clearStale')));
309
- yield* Console.log('Cleared stale test runs.');
310
- })).pipe(Effect.catchTag('SqlError', e => Effect.fail(RepositoryWriteError.from('clearStale')(e))));
311
- const clearUncommitedTestResults = ({
312
- name
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* sql`
327
- DELETE FROM ${sql(tables.testRunResults)}
328
- WHERE testRun = ${currentTestRun.value.id};
329
- `.pipe(Effect.mapError(RepositoryWriteError.from('clearUncommitedTestResults')));
330
- yield* sql`
331
- DELETE FROM ${sql(tables.testResults)}
332
- WHERE id NOT IN (
333
- SELECT DISTINCT testResult FROM ${sql(tables.testRunResults)}
334
- );
335
- `.pipe(Effect.mapError(RepositoryWriteError.from('clearUncommitedTestResults')));
336
- })).pipe(Effect.catchTag('SqlError', e => Effect.fail(RepositoryWriteError.from('clearUncommitedTestResults')(e))));
337
- const commitCurrentTestRun = Effect.fnUntraced(function* ({
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
- hash
450
+ variant,
451
+ before,
452
+ limit
340
453
  }) {
341
- const currentTestRunResults = yield* Schema.decodeUnknownEffect(CountSchema)(yield* sql`
342
- SELECT COUNT(*) count FROM ${sql(tables.testRunResults)}
343
- WHERE testRun = (
344
- SELECT id
345
- FROM ${sql(tables.testRuns)}
346
- WHERE hash IS NULL AND name = ${name}
347
- );
348
- `.pipe(Effect.mapError(RepositoryQueryError.fromSql('commitCurrentTestRun')))).pipe(Effect.mapError(RepositoryDecodeError.from('commitCurrentTestRun')));
349
- if (currentTestRunResults === 0) {
350
- yield* Console.log('Current run has no test results, not commiting.');
351
- return;
352
- }
353
- yield* sql`
354
- UPDATE ${sql(tables.testRuns)}
355
- SET hash = ${hash}
356
- WHERE id = (
357
- SELECT id
358
- FROM ${sql(tables.testRuns)}
359
- WHERE hash IS NULL AND name = ${name}
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 service = {
371
- clearStale,
372
- clearUncommitedTestResults,
373
- commitCurrentTestRun,
374
- getAllTestResults,
375
- getAllTestRuns,
376
- getOrCreateCurrentTestRun,
377
- getLastTestRunHash,
378
- getPreviousTestRun,
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
- hasResults,
381
- insertTestResult,
382
- insertTestResults,
383
- getAllTestRunResults
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