@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
@@ -3,398 +3,529 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- Object.defineProperty(exports, "TestRepository", {
6
+ exports.makeTestRepository = exports.TestResultStorage = exports.TestRepositoryStorage = void 0;
7
+ Object.defineProperty(exports, "tables", {
7
8
  enumerable: true,
8
9
  get: function () {
9
- return _TestRepository.TestRepository;
10
+ return _testRepositorySchema.tables;
10
11
  }
11
12
  });
12
- exports.tables = exports.makeTestRepository = void 0;
13
- var SqlClient = /*#__PURE__*/_interopRequireWildcard(/*#__PURE__*/require("effect/sql/SqlClient"));
13
+ var Sqlite = /*#__PURE__*/_interopRequireWildcard(/*#__PURE__*/require("@effect/sql-sqlite-node"));
14
14
  var _effect = /*#__PURE__*/require("effect");
15
- var _TestRepository = /*#__PURE__*/require("../Test.repository.js");
16
- var _TestRepositoryTypes = /*#__PURE__*/require("../Test.repository.types.js");
15
+ var _sql = /*#__PURE__*/require("effect/sql");
16
+ var _TestRun = /*#__PURE__*/require("../TestRun.js");
17
17
  var _RepositoryError = /*#__PURE__*/require("../RepositoryError.js");
18
18
  var _Classify = /*#__PURE__*/require("./Classify.js");
19
- var _Schema = /*#__PURE__*/require("./lib/Schema.js");
19
+ var _testRepositorySchema = /*#__PURE__*/require("./test-repository-schema.js");
20
+ var _testResultStats = /*#__PURE__*/require("./test-result-stats.js");
21
+ var _TestSerialization = /*#__PURE__*/require("./Test.serialization.js");
20
22
  var cache;
21
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; }
22
- const tables = exports.tables = {
23
- testResults: 'test-results',
24
- testRuns: 'test-runs',
25
- testRunResults: 'test-run-results'
26
- };
27
- const TestResultWriteSchema = /*#__PURE__*/_effect.Schema.Struct({
24
+ class TestRepositoryStorage extends /*#__PURE__*/_effect.Context.Service()('@creative-introvert/tons-of-tests/internal/TestRepositoryStorage') {}
25
+ exports.TestRepositoryStorage = TestRepositoryStorage;
26
+ const TestResultStorage = exports.TestResultStorage = /*#__PURE__*/_effect.Schema.Struct({
28
27
  id: _effect.Schema.String,
29
28
  hashTestCase: _effect.Schema.String,
30
29
  key: _effect.Schema.String,
31
- ordering: _effect.Schema.Int,
32
- resultOrdering: _effect.Schema.Int,
33
- input: /*#__PURE__*/_effect.Schema.fromJsonString(_effect.Schema.Unknown),
34
- result: /*#__PURE__*/_effect.Schema.fromJsonString(_effect.Schema.Unknown),
35
- expected: /*#__PURE__*/_effect.Schema.fromJsonString(_effect.Schema.Unknown),
36
- label: _Classify.LabelSchema,
37
- tags: /*#__PURE__*/(0, _Schema.split)(','),
38
- timeMillis: _effect.Schema.Finite
30
+ input: /*#__PURE__*/_effect.Schema.fromJsonString(_effect.Schema.Json),
31
+ result: /*#__PURE__*/_effect.Schema.fromJsonString(_effect.Schema.Json),
32
+ expected: /*#__PURE__*/_effect.Schema.fromJsonString(_effect.Schema.Json),
33
+ label: _Classify.LabelSchema
39
34
  });
40
- const TestResultsSchema = /*#__PURE__*/_effect.Schema.Array(TestResultWriteSchema);
41
- const CountSchema = /*#__PURE__*/_effect.Schema.Tuple([_effect.Schema.Struct({
42
- count: _effect.Schema.Int
43
- })]).pipe(/*#__PURE__*/_effect.Schema.decodeTo(_effect.Schema.Int, /*#__PURE__*/_effect.SchemaTransformation.transform({
44
- decode: ([{
45
- count
46
- }]) => count,
47
- encode: count => [{
48
- count
49
- }]
50
- })));
51
- const TestRunsReadSchema = /*#__PURE__*/_effect.Schema.Array(_TestRepositoryTypes.TestRun);
52
- const TestRunResultsSchema = /*#__PURE__*/_effect.Schema.Array(/*#__PURE__*/_effect.Schema.Struct({
53
- testRun: _effect.Schema.Int,
54
- testResult: _effect.Schema.String,
35
+ const StoredResult = /*#__PURE__*/_effect.Schema.Struct({
36
+ ...TestResultStorage.fields,
37
+ input: _effect.Schema.String,
38
+ result: _effect.Schema.String,
39
+ expected: _effect.Schema.String,
55
40
  ordering: _effect.Schema.Int,
56
41
  resultOrdering: _effect.Schema.Int,
42
+ tags: /*#__PURE__*/_effect.Schema.fromJsonString(/*#__PURE__*/_effect.Schema.Array(_effect.Schema.String)),
57
43
  timeMillis: _effect.Schema.Finite
44
+ });
45
+ const RawResult = /*#__PURE__*/StoredResult.mapFields(fields => ({
46
+ ...fields,
47
+ tags: _effect.Schema.String
58
48
  }));
59
- const TableColumnsSchema = /*#__PURE__*/_effect.Schema.Array(/*#__PURE__*/_effect.Schema.Struct({
60
- name: _effect.Schema.String
61
- }));
62
- const makeTestRepository = exports.makeTestRepository = /*#__PURE__*/_effect.Effect.gen(function* () {
63
- const sql = yield* SqlClient.SqlClient;
64
- yield* sql`PRAGMA auto_vacuum = FULL;`;
65
- yield* sql.withTransaction(_effect.Effect.gen(function* () {
66
- yield* sql`
67
- CREATE TABLE IF NOT EXISTS ${sql(tables.testRuns)}(
68
- id INTEGER PRIMARY KEY AUTOINCREMENT,
69
- name TEXT NOT NULL,
70
- hash TEXT
71
- );
72
- `;
73
- yield* sql`CREATE INDEX IF NOT EXISTS idx_name_hash ON ${sql(tables.testRuns)}(name, hash)`;
74
- // At most one "current" (hash IS NULL) row per suite name. Partial index
75
- // so committed rows are unaffected.
76
- yield* sql`
77
- CREATE UNIQUE INDEX IF NOT EXISTS idx_current_per_name
78
- ON ${sql(tables.testRuns)}(name)
79
- WHERE hash IS NULL;
80
- `;
81
- yield* sql`
82
- CREATE TABLE IF NOT EXISTS ${sql(tables.testResults)}(
83
- id TEXT PRIMARY KEY,
84
- hashTestCase TEXT NOT NULL,
85
- key TEXT NOT NULL DEFAULT 'result',
86
- ordering INTEGER NOT NULL,
87
- resultOrdering INTEGER NOT NULL DEFAULT 0,
88
- input TEXT,
89
- result TEXT,
90
- expected TEXT,
91
- label TEXT NOT NULL,
92
- tags TEXT NOT NULL,
93
- timeMillis INTEGER NOT NULL
94
- );
95
- `;
96
- yield* sql`
97
- CREATE TABLE IF NOT EXISTS ${sql(tables.testRunResults)}(
98
- testRun INTEGER NOT NULL,
99
- testResult TEXT NOT NULL,
100
- ordering INTEGER NOT NULL,
101
- resultOrdering INTEGER NOT NULL,
102
- timeMillis INTEGER NOT NULL,
103
- FOREIGN KEY (testRun) REFERENCES ${sql(tables.testRuns)}(id),
104
- FOREIGN KEY (testResult) REFERENCES ${sql(tables.testResults)}(id),
105
- UNIQUE (testRun, testResult)
106
- );
107
- `;
108
- const resultColumns = yield* _effect.Schema.decodeUnknownEffect(TableColumnsSchema)(yield* sql`PRAGMA table_info(${sql(tables.testResults)});`).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('migrateTestResults')));
109
- if (!resultColumns.some(column => column.name === 'key')) {
110
- yield* sql`
111
- ALTER TABLE ${sql(tables.testResults)}
112
- ADD COLUMN key TEXT NOT NULL DEFAULT 'result';
113
- `;
114
- }
115
- if (!resultColumns.some(column => column.name === 'resultOrdering')) {
116
- yield* sql`
117
- ALTER TABLE ${sql(tables.testResults)}
118
- ADD COLUMN resultOrdering INTEGER NOT NULL DEFAULT 0;
119
- `;
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
+ }))]);
54
+ const encodeResult = /*#__PURE__*/_effect.Schema.encodeEffect(StoredResult);
55
+ const decodeRows = /*#__PURE__*/_effect.Schema.decodeUnknownEffect(/*#__PURE__*/_effect.Schema.Array(StoredResult));
56
+ const decodeRawRows = /*#__PURE__*/_effect.Schema.decodeUnknownEffect(/*#__PURE__*/_effect.Schema.Array(RawResult));
57
+ const decodeRuns = /*#__PURE__*/_effect.Schema.decodeUnknownEffect(/*#__PURE__*/_effect.Schema.Array(_TestRun.TestRun));
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({
60
+ isBaseline: /*#__PURE__*/_effect.Schema.Literals([0, 1])
61
+ }))));
62
+ const decodeVariantBaselines = /*#__PURE__*/_effect.Schema.decodeUnknownEffect(/*#__PURE__*/_effect.Schema.Array(/*#__PURE__*/_effect.Schema.Struct({
63
+ variant: _effect.Schema.String,
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'))
67
+ })));
68
+ const decodeRunLimit = /*#__PURE__*/_effect.Schema.decodeUnknownEffect(/*#__PURE__*/_effect.Schema.Int.check(/*#__PURE__*/_effect.Schema.isBetween({
69
+ minimum: 1,
70
+ maximum: 100
71
+ })));
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
+ });
120
99
  }
121
- const runResultColumns = yield* _effect.Schema.decodeUnknownEffect(TableColumnsSchema)(yield* sql`PRAGMA table_info(${sql(tables.testRunResults)});`).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('migrateTestRunResults')));
122
- for (const column of ['ordering', 'resultOrdering', 'timeMillis']) {
123
- if (runResultColumns.some(existing => existing.name === column)) {
124
- continue;
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;
125
124
  }
126
- yield* sql`
127
- ALTER TABLE ${sql(tables.testRunResults)}
128
- ADD COLUMN ${sql(column)} INTEGER NOT NULL DEFAULT 0;
129
- `;
130
- yield* sql`
131
- UPDATE ${sql(tables.testRunResults)} AS runres
132
- SET ${sql(column)} = (
133
- SELECT ${sql(column)} FROM ${sql(tables.testResults)} res
134
- WHERE res.id = runres.testResult
135
- );
136
- `;
137
125
  }
138
- // Lookup by the junction's `testResult` column: the existing
139
- // UNIQUE(testRun, testResult) index is keyed on testRun first, so orphan
140
- // cleanup (`... NOT IN (SELECT testResult ...)`) and stream joins
141
- // (`res.id = runres.testResult`) would otherwise full-scan the junction.
142
- yield* sql`CREATE INDEX IF NOT EXISTS idx_trr_testresult ON ${sql(tables.testRunResults)}(testResult)`;
143
- yield* sql`CREATE INDEX IF NOT EXISTS idx_tr_hashtestcase ON ${sql(tables.testResults)}(hashTestCase)`;
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;
144
143
  }));
145
- // With 11 result columns and 5 junction columns, batches of 50 rows stay below
146
- // SQLite's lowest default limit of 999 bound values per statement.
147
- const INSERT_BATCH_SIZE = 50;
148
- const insertTestResults = (testResults, testRun) => sql.withTransaction(_effect.Effect.gen(function* () {
149
- if (testResults.length === 0) return;
150
- const encoded = yield* _effect.Effect.forEach(testResults, _ => _effect.Schema.encodeEffect(TestResultWriteSchema)(_).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('insertTestResults'))));
151
- for (let i = 0; i < encoded.length; i += INSERT_BATCH_SIZE) {
152
- const chunk = encoded.slice(i, i + INSERT_BATCH_SIZE);
153
- yield* sql`
154
- INSERT INTO ${sql(tables.testResults)}
155
- ${sql.insert(chunk)}
156
- ON CONFLICT(id) DO NOTHING;
157
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryWriteError.from('insertTestResults')));
158
- }
159
- // Junction rows reference the resolved testRun.id directly,
160
- // so no correlated subquery.
161
- const junction = encoded.map(_ => ({
162
- testRun: testRun.id,
163
- testResult: _.id,
164
- ordering: _.ordering,
165
- resultOrdering: _.resultOrdering,
166
- timeMillis: _.timeMillis
167
- }));
168
- for (let i = 0; i < junction.length; i += INSERT_BATCH_SIZE) {
169
- const chunk = junction.slice(i, i + INSERT_BATCH_SIZE);
170
- yield* sql`
171
- INSERT INTO ${sql(tables.testRunResults)}
172
- ${sql.insert(chunk)}
173
- ON CONFLICT(testRun, testResult) DO UPDATE SET
174
- ordering = excluded.ordering,
175
- resultOrdering = excluded.resultOrdering,
176
- timeMillis = excluded.timeMillis;
177
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryWriteError.from('insertTestResults')));
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;
178
165
  }
179
- })).pipe(_effect.Effect.catchTag('SqlError', e => _effect.Effect.fail(_RepositoryError.RepositoryWriteError.from('insertTestResults')(e))));
180
- const insertTestResult = (testResult, testRun) => insertTestResults([testResult], testRun);
181
- const makeTestResultSchema = schemas => {
182
- const input = schemas?.input ?? _effect.Schema.Unknown;
183
- const result = schemas?.result ?? _effect.Schema.Unknown;
184
- const expected = schemas?.expected ?? _effect.Schema.Unknown;
185
- return _effect.Schema.Struct({
186
- id: _effect.Schema.String,
187
- hashTestCase: _effect.Schema.String,
188
- key: _effect.Schema.String,
189
- ordering: _effect.Schema.Int,
190
- resultOrdering: _effect.Schema.Int,
191
- input: _effect.Schema.fromJsonString(input),
192
- result: _effect.Schema.fromJsonString(result),
193
- expected: _effect.Schema.fromJsonString(expected),
194
- label: _Classify.LabelSchema,
195
- tags: (0, _Schema.split)(','),
196
- timeMillis: _effect.Schema.Finite
166
+ const sql = yield* openClient({
167
+ filename: options.cachePath,
168
+ readonly: true,
169
+ disableWAL: true
197
170
  });
198
- };
199
- const getTestResultsStream = (testRun, schemas) => {
200
- const RowSchema = makeTestResultSchema(schemas);
201
- const RowsSchema = _effect.Schema.Array(RowSchema);
202
- // The SQLite driver returns rows in one batch. Decode that batch once
203
- // before exposing it as a stream.
204
- return _effect.Stream.unwrap(_effect.Effect.gen(function* () {
205
- const rows = yield* sql`
206
- SELECT res.id, res.hashTestCase, res.key,
207
- runres.ordering, runres.resultOrdering,
208
- res.input, res.result, res.expected, res.label, res.tags,
209
- runres.timeMillis
210
- FROM ${sql(tables.testResults)} res
211
- JOIN ${sql(tables.testRunResults)} runres ON res.id = runres.testResult
212
- JOIN ${sql(tables.testRuns)} runs ON runs.id = runres.testRun
213
- WHERE runs.id = ${testRun.id}
214
- ORDER BY runres.ordering ASC, runres.resultOrdering ASC;
215
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryQueryError.fromSql('getTestResultsStream')));
216
- const decoded = yield* _effect.Schema.decodeUnknownEffect(RowsSchema)(rows).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('getTestResultsStream')));
217
- return _effect.Stream.fromIterable(decoded);
218
- }));
219
- };
220
- const hasResults = _effect.Effect.fnUntraced(function* (testRun) {
221
- const b = yield* sql`
222
- SELECT COUNT(*) count FROM ${sql(tables.testRunResults)}
223
- WHERE testRun = ${testRun.id};
224
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryQueryError.fromSql('hasResults')));
225
- const r = yield* _effect.Schema.decodeUnknownEffect(CountSchema)(b).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('hasResults')));
226
- return r > 0;
171
+ // Keep the selected rows stable until the scoped connection closes,
172
+ // including when reading is interrupted.
173
+ yield* sql`BEGIN;`;
174
+ return sql;
227
175
  });
228
- const getPreviousTestRun = _effect.Effect.fnUntraced(function* (name) {
229
- const r = yield* sql`
230
- SELECT * FROM ${sql(tables.testRuns)}
231
- WHERE hash IS NOT NULL AND name = ${name}
232
- ORDER BY id DESC
233
- LIMIT 1;
234
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryQueryError.fromSql('getPreviousTestRun')));
235
- const p = yield* _effect.Schema.decodeUnknownEffect(TestRunsReadSchema)(r).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('getPreviousTestRun')));
236
- return _effect.Array.head(p);
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
+ `));
237
183
  });
238
- const getAllTestRuns = _effect.Effect.gen(function* () {
239
- const results = yield* sql`
240
- SELECT * from ${sql(tables.testRuns)};
241
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryQueryError.fromSql('getAllTestRuns')));
242
- return yield* _effect.Schema.decodeUnknownEffect(TestRunsReadSchema)(results).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('getAllTestRuns')));
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;
243
193
  });
244
- const getAllTestRunResults = _effect.Effect.gen(function* () {
245
- const results = yield* sql`
246
- SELECT * from ${sql(tables.testRunResults)};
247
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryQueryError.fromSql('getAllTestRunResults')));
248
- return yield* _effect.Schema.decodeUnknownEffect(TestRunResultsSchema)(results).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('getAllTestRunResults')));
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
+ `;
249
219
  });
250
- const getAllTestResults = _effect.Effect.gen(function* () {
251
- const results = yield* sql`
252
- SELECT * from ${sql(tables.testResults)};
253
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryQueryError.fromSql('getAllTestResults')));
254
- return yield* _effect.Schema.decodeUnknownEffect(TestResultsSchema)(results).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('getAllTestResults')));
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
+ }
224
+ const codec = (0, _TestSerialization.makeResultCodec)(schemas, run.name);
225
+ // Fifty rows keep every statement below SQLite's 999-parameter minimum.
226
+ for (let offset = 0; offset < Math.max(1, results.length); offset += 50) {
227
+ const encoded = yield* _effect.Effect.forEach(results.slice(offset, offset + 50), _effect.Effect.fnUntraced(function* (result) {
228
+ const encoded = yield* codec.encode(result);
229
+ return yield* encodeResult({
230
+ ...encoded,
231
+ input: (0, _TestSerialization.stringifyJson)(encoded.input),
232
+ result: (0, _TestSerialization.stringifyJson)(encoded.result),
233
+ expected: (0, _TestSerialization.stringifyJson)(encoded.expected)
234
+ });
235
+ }));
236
+ yield* writeCache(_effect.Effect.gen(function* () {
237
+ const saved = yield* requireRun(cache, run);
238
+ if (saved.state !== 'Running') {
239
+ return yield* _effect.Effect.die(new Error('Only Running test runs accept result writes.'));
240
+ }
241
+ yield* writeRows(cache, run.id, encoded);
242
+ }));
243
+ }
255
244
  });
256
- const getLastTestRunHash = _effect.Effect.fnUntraced(function* (name) {
257
- const rows = yield* sql`
258
- SELECT *
259
- FROM ${sql(tables.testRuns)}
260
- WHERE hash IS NOT NULL AND name = ${name}
261
- ORDER BY id DESC
262
- LIMIT 1;
263
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryQueryError.fromSql('getLastTestRunHash')));
264
- const runs = yield* _effect.Schema.decodeUnknownEffect(TestRunsReadSchema)(rows).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('getLastTestRunHash')));
265
- return _effect.Array.head(runs).pipe(_effect.Option.flatMap(run => _effect.Option.fromNullishOr(run.hash)));
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
+ };
260
+ const getTestResultsStream = (run, schemas, matching) => {
261
+ if (matching !== undefined && matching.length > 50) return _effect.Stream.die(new Error('Match at most 50 case/key pairs per query.'));
262
+ const codec = (0, _TestSerialization.makeResultCodec)(schemas, run.name);
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
+ }));
275
+ }));
276
+ };
277
+ const getLatestTestRun = _effect.Effect.fnUntraced(function* ({
278
+ name,
279
+ variant
280
+ }) {
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
+ `));
266
286
  });
267
- const getOrCreateCurrentTestRun = name => sql.withTransaction(_effect.Effect.gen(function* () {
268
- const currentRaw = yield* sql`
269
- SELECT *
270
- FROM ${sql(tables.testRuns)}
271
- WHERE hash IS NULL AND name = ${name}
272
- ORDER BY id DESC
273
- LIMIT 1;
274
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryQueryError.fromSql('getOrCreateCurrentTestRun')));
275
- const current = yield* _effect.Schema.decodeUnknownEffect(TestRunsReadSchema)(currentRaw).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('getOrCreateCurrentTestRun')));
276
- const existing = _effect.Array.head(current);
277
- if (_effect.Option.isSome(existing)) {
278
- return existing.value;
279
- }
280
- const insertedRaw = yield* sql`
281
- INSERT INTO ${sql(tables.testRuns)}
282
- ${sql.insert({
283
- name
284
- })}
285
- RETURNING *;
286
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryWriteError.from('getOrCreateCurrentTestRun')));
287
- const inserted = yield* _effect.Schema.decodeUnknownEffect(TestRunsReadSchema)(insertedRaw).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('getOrCreateCurrentTestRun')));
288
- return yield* _effect.Effect.fromOption(_effect.Array.head(inserted)).pipe(_effect.Effect.mapError(() => new _RepositoryError.TestRunNotCreated({
289
- testSuiteName: name
290
- })));
291
- })).pipe(_effect.Effect.catchTag('SqlError', e => _effect.Effect.fail(_RepositoryError.RepositoryQueryError.fromSql('getOrCreateCurrentTestRun')(e))));
292
- const clearStale = ({
287
+ const getBaselineTestRun = ({
293
288
  name,
294
- keep = 1
295
- }) => sql.withTransaction(_effect.Effect.gen(function* () {
296
- const raw = yield* sql`
297
- SELECT *
298
- FROM ${sql(tables.testRuns)}
299
- WHERE hash IS NOT NULL AND name = ${name}
300
- ORDER BY id DESC
301
- LIMIT -1 OFFSET ${keep};
302
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryQueryError.fromSql('clearStale')));
303
- const staleTestRuns = yield* _effect.Schema.decodeUnknownEffect(TestRunsReadSchema)(raw).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('clearStale')));
304
- if (staleTestRuns.length === 0) {
305
- return;
289
+ variant
290
+ }) => withSnapshot(_effect.Option.none(), _effect.Effect.fnUntraced(function* (sql) {
291
+ return _effect.Array.head(yield* decodeRuns(yield* sql`
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
+ }));
297
+ const createTestRun = _effect.Effect.fnUntraced(function* (input) {
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;
318
+ `);
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
+ }));
334
+ const withTestRun = (input, execute) => _effect.Effect.acquireUseRelease(createTestRun(input), run => _effect.Effect.suspend(() => execute(run)).pipe(_effect.Effect.map(value => ({
335
+ run,
336
+ value
337
+ }))), (run, exit) => finishTestRun(run, _effect.Exit.isSuccess(exit) ? 'Complete' : 'Failed')).pipe(_effect.Effect.map(({
338
+ run,
339
+ value
340
+ }) => ({
341
+ run: _TestRun.TestRun.make({
342
+ ...run,
343
+ state: 'Complete'
344
+ }),
345
+ value
346
+ })));
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
362
+ });
306
363
  }
307
- const ids = staleTestRuns.map(r => r.id);
308
- yield* sql`
309
- DELETE FROM ${sql(tables.testRunResults)}
310
- WHERE testRun IN ${sql.in(ids)};
311
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryWriteError.from('clearStale')));
312
- yield* sql`
313
- DELETE FROM ${sql(tables.testRuns)}
314
- WHERE id IN ${sql.in(ids)};
315
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryWriteError.from('clearStale')));
316
- yield* sql`
317
- DELETE FROM ${sql(tables.testResults)}
318
- WHERE id NOT IN (
319
- SELECT DISTINCT testResult FROM ${sql(tables.testRunResults)}
320
- );
321
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryWriteError.from('clearStale')));
322
- yield* _effect.Console.log('Cleared stale test runs.');
323
- })).pipe(_effect.Effect.catchTag('SqlError', e => _effect.Effect.fail(_RepositoryError.RepositoryWriteError.from('clearStale')(e))));
324
- const clearUncommitedTestResults = ({
325
- name
326
- }) => sql.withTransaction(_effect.Effect.gen(function* () {
327
- const currentRaw = yield* sql`
328
- SELECT *
329
- FROM ${sql(tables.testRuns)}
330
- WHERE hash IS NULL AND name = ${name}
331
- ORDER BY id DESC
332
- LIMIT 1;
333
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryQueryError.fromSql('clearUncommitedTestResults')));
334
- const current = yield* _effect.Schema.decodeUnknownEffect(TestRunsReadSchema)(currentRaw).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('clearUncommitedTestResults')));
335
- const currentTestRun = _effect.Array.head(current);
336
- if (_effect.Option.isNone(currentTestRun)) {
337
- return;
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({
379
+ testSuiteName: run.name,
380
+ variant: run.variant
381
+ });
382
+ selectedVariants.add(key);
338
383
  }
339
- yield* sql`
340
- DELETE FROM ${sql(tables.testRunResults)}
341
- WHERE testRun = ${currentTestRun.value.id};
342
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryWriteError.from('clearUncommitedTestResults')));
343
- yield* sql`
344
- DELETE FROM ${sql(tables.testResults)}
345
- WHERE id NOT IN (
346
- SELECT DISTINCT testResult FROM ${sql(tables.testRunResults)}
347
- );
348
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryWriteError.from('clearUncommitedTestResults')));
349
- })).pipe(_effect.Effect.catchTag('SqlError', e => _effect.Effect.fail(_RepositoryError.RepositoryWriteError.from('clearUncommitedTestResults')(e))));
350
- const commitCurrentTestRun = _effect.Effect.fnUntraced(function* ({
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
+ }));
462
+ const listTestRuns = _effect.Effect.fnUntraced(function* ({
351
463
  name,
352
- hash
464
+ variant,
465
+ before,
466
+ limit
353
467
  }) {
354
- const currentTestRunResults = yield* _effect.Schema.decodeUnknownEffect(CountSchema)(yield* sql`
355
- SELECT COUNT(*) count FROM ${sql(tables.testRunResults)}
356
- WHERE testRun = (
357
- SELECT id
358
- FROM ${sql(tables.testRuns)}
359
- WHERE hash IS NULL AND name = ${name}
360
- );
361
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryQueryError.fromSql('commitCurrentTestRun')))).pipe(_effect.Effect.mapError(_RepositoryError.RepositoryDecodeError.from('commitCurrentTestRun')));
362
- if (currentTestRunResults === 0) {
363
- yield* _effect.Console.log('Current run has no test results, not commiting.');
364
- return;
365
- }
366
- yield* sql`
367
- UPDATE ${sql(tables.testRuns)}
368
- SET hash = ${hash}
369
- WHERE id = (
370
- SELECT id
371
- FROM ${sql(tables.testRuns)}
372
- WHERE hash IS NULL AND name = ${name}
373
- );
374
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryWriteError.from('commitCurrentTestRun')));
375
- yield* sql`
376
- INSERT INTO ${sql(tables.testRuns)}
377
- ${sql.insert({
378
- name
379
- })};
380
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryWriteError.from('commitCurrentTestRun')));
381
- yield* _effect.Console.log('Commited test run.');
468
+ const parsedLimit = yield* decodeRunLimit(limit);
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
475
+ WHERE run.name = ${name}
476
+ AND ${variant === undefined ? sql`1 = 1` : sql`run.variant = ${variant}`}
477
+ AND ${before === undefined ? sql`1 = 1` : sql`run.id < ${before}`}
478
+ ORDER BY run.id DESC LIMIT ${parsedLimit};
479
+ `);
480
+ return rows.map(({
481
+ isBaseline,
482
+ ...run
483
+ }) => ({
484
+ ...run,
485
+ isBaseline: isBaseline === 1
486
+ }));
487
+ }));
382
488
  });
383
- const service = {
384
- clearStale,
385
- clearUncommitedTestResults,
386
- commitCurrentTestRun,
387
- getAllTestResults,
388
- getAllTestRuns,
389
- getOrCreateCurrentTestRun,
390
- getLastTestRunHash,
391
- getPreviousTestRun,
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(({
498
+ variant,
499
+ id,
500
+ name
501
+ }) => ({
502
+ variant,
503
+ baseline: id === null || name === null ? _effect.Option.none() : _effect.Option.some(_TestRun.TestRun.make({
504
+ id,
505
+ name,
506
+ variant,
507
+ store: 'snapshot',
508
+ state: 'Complete'
509
+ }))
510
+ }));
511
+ }));
512
+ const repository = {
513
+ commitTestRuns,
514
+ getTestRun,
515
+ getLatestTestRun,
516
+ getBaselineTestRun,
517
+ listTestRuns,
518
+ listVariantBaselines,
392
519
  getTestResultsStream,
393
- hasResults,
394
- insertTestResult,
395
- insertTestResults,
396
- getAllTestRunResults
520
+ withTestRun,
521
+ withStats: use => (0, _testResultStats.withStats)(use).pipe(_effect.Effect.provideService(_sql.SqlClient.SqlClient, cache)),
522
+ insertTestResult: (result, run, schemas) => insertTestResults([result], run, schemas),
523
+ insertTestResults
524
+ };
525
+ return {
526
+ repository,
527
+ cache,
528
+ snapshot: getMemorySnapshot
397
529
  };
398
- return service;
399
530
  });
400
531
  //# sourceMappingURL=Test.repository.sqlite.js.map