@creative-introvert/tons-of-tests 3.0.0-rc.0 → 3.0.0-rc.2

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