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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +24 -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 +37 -2
  9. package/dist/cjs/Test.js.map +1 -1
  10. package/dist/cjs/Test.repository.js +8 -17
  11. package/dist/cjs/Test.repository.js.map +1 -1
  12. package/dist/cjs/TestRun.js +20 -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 +236 -93
  21. package/dist/cjs/internal/Test.js.map +1 -1
  22. package/dist/cjs/internal/Test.repository.sqlite.js +262 -357
  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-migrations.js +68 -0
  34. package/dist/cjs/internal/test-repository-migrations.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 +19 -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 +120 -14
  44. package/dist/dts/Test.d.ts.map +1 -1
  45. package/dist/dts/Test.repository.d.ts +94 -8
  46. package/dist/dts/Test.repository.d.ts.map +1 -1
  47. package/dist/dts/TestRun.d.ts +18 -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 +52 -30
  56. package/dist/dts/internal/Test.d.ts.map +1 -1
  57. package/dist/dts/internal/Test.repository.sqlite.d.ts +66 -12
  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 +15 -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 +22 -0
  68. package/dist/dts/internal/test-repository-fixtures.d.ts.map +1 -0
  69. package/dist/dts/internal/test-repository-migrations.d.ts +11 -0
  70. package/dist/dts/internal/test-repository-migrations.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 +14 -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 +34 -1
  80. package/dist/esm/Test.js.map +1 -1
  81. package/dist/esm/Test.repository.js +7 -16
  82. package/dist/esm/Test.repository.js.map +1 -1
  83. package/dist/esm/TestRun.js +13 -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 +232 -92
  92. package/dist/esm/internal/Test.js.map +1 -1
  93. package/dist/esm/internal/Test.repository.sqlite.js +261 -354
  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-migrations.js +62 -0
  105. package/dist/esm/internal/test-repository-migrations.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,303 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- Object.defineProperty(exports, "TestRepository", {
6
+ exports.makeTestRepository = exports.TestResultStorage = void 0;
7
+ Object.defineProperty(exports, "tables", {
7
8
  enumerable: true,
8
9
  get: function () {
9
- return _TestRepository.TestRepository;
10
+ return _testRepositoryMigrations.tables;
10
11
  }
11
12
  });
12
- exports.tables = exports.makeTestRepository = void 0;
13
- var SqlClient = /*#__PURE__*/_interopRequireWildcard(/*#__PURE__*/require("effect/sql/SqlClient"));
14
13
  var _effect = /*#__PURE__*/require("effect");
15
- var _TestRepository = /*#__PURE__*/require("../Test.repository.js");
16
- var _TestRepositoryTypes = /*#__PURE__*/require("../Test.repository.types.js");
14
+ var _sql = /*#__PURE__*/require("effect/sql");
15
+ var _TestRun = /*#__PURE__*/require("../TestRun.js");
17
16
  var _RepositoryError = /*#__PURE__*/require("../RepositoryError.js");
18
17
  var _Classify = /*#__PURE__*/require("./Classify.js");
19
- var _Schema = /*#__PURE__*/require("./lib/Schema.js");
20
- var cache;
21
- 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({
18
+ var _testRepositoryMigrations = /*#__PURE__*/require("./test-repository-migrations.js");
19
+ var _testResultStats = /*#__PURE__*/require("./test-result-stats.js");
20
+ var _TestSerialization = /*#__PURE__*/require("./Test.serialization.js");
21
+ const TestResultStorage = exports.TestResultStorage = /*#__PURE__*/_effect.Schema.Struct({
28
22
  id: _effect.Schema.String,
29
23
  hashTestCase: _effect.Schema.String,
30
24
  key: _effect.Schema.String,
25
+ input: /*#__PURE__*/_effect.Schema.fromJsonString(_effect.Schema.Json),
26
+ result: /*#__PURE__*/_effect.Schema.fromJsonString(_effect.Schema.Json),
27
+ expected: /*#__PURE__*/_effect.Schema.fromJsonString(_effect.Schema.Json),
28
+ label: _Classify.LabelSchema
29
+ });
30
+ const StoredResult = /*#__PURE__*/_effect.Schema.Struct({
31
+ ...TestResultStorage.fields,
32
+ input: _effect.Schema.String,
33
+ result: _effect.Schema.String,
34
+ expected: _effect.Schema.String,
31
35
  ordering: _effect.Schema.Int,
32
36
  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)(','),
37
+ tags: /*#__PURE__*/_effect.Schema.fromJsonString(/*#__PURE__*/_effect.Schema.Array(_effect.Schema.String)),
38
38
  timeMillis: _effect.Schema.Finite
39
39
  });
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
- }]
40
+ const encodeResult = /*#__PURE__*/_effect.Schema.encodeEffect(StoredResult);
41
+ const decodeRows = /*#__PURE__*/_effect.Schema.decodeUnknownEffect(/*#__PURE__*/_effect.Schema.Array(StoredResult));
42
+ 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,
45
+ isBaseline: /*#__PURE__*/_effect.Schema.Literals([0, 1])
46
+ })));
47
+ const decodeVariantBaselines = /*#__PURE__*/_effect.Schema.decodeUnknownEffect(/*#__PURE__*/_effect.Schema.Array(/*#__PURE__*/_effect.Schema.Struct({
48
+ 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)
52
+ })));
53
+ const decodeRunLimit = /*#__PURE__*/_effect.Schema.decodeUnknownEffect(/*#__PURE__*/_effect.Schema.Int.check(/*#__PURE__*/_effect.Schema.isBetween({
54
+ minimum: 1,
55
+ maximum: 100
50
56
  })));
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,
55
- ordering: _effect.Schema.Int,
56
- resultOrdering: _effect.Schema.Int,
57
- timeMillis: _effect.Schema.Finite
58
- }));
59
- const TableColumnsSchema = /*#__PURE__*/_effect.Schema.Array(/*#__PURE__*/_effect.Schema.Struct({
60
- name: _effect.Schema.String
61
- }));
62
57
  const makeTestRepository = exports.makeTestRepository = /*#__PURE__*/_effect.Effect.gen(function* () {
63
- const sql = yield* SqlClient.SqlClient;
58
+ const sql = yield* _sql.SqlClient.SqlClient;
64
59
  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
- `;
120
- }
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;
125
- }
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
- }
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)`;
144
- }));
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)}
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};`));
63
+ });
64
+ const insertTestResults = _effect.Effect.fnUntraced(function* (results, run, schemas) {
65
+ const codec = (0, _TestSerialization.makeResultCodec)(schemas, run.name);
66
+ // Each statement stays below SQLite's lowest default limit of 999 parameters.
67
+ for (let offset = 0; offset < Math.max(1, results.length); offset += 50) {
68
+ const encoded = yield* _effect.Effect.forEach(results.slice(offset, offset + 50), _effect.Effect.fnUntraced(function* (result) {
69
+ const encoded = yield* codec.encode(result);
70
+ return yield* encodeResult({
71
+ ...encoded,
72
+ input: (0, _TestSerialization.stringifyJson)(encoded.input),
73
+ result: (0, _TestSerialization.stringifyJson)(encoded.result),
74
+ expected: (0, _TestSerialization.stringifyJson)(encoded.expected)
75
+ });
76
+ }));
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') {
85
+ return yield* _effect.Effect.die(new Error('Only Running test runs accept result writes.'));
86
+ }
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)}
156
99
  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)}
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)}
173
111
  ON CONFLICT(testRun, testResult) DO UPDATE SET
174
112
  ordering = excluded.ordering,
175
113
  resultOrdering = excluded.resultOrdering,
176
- timeMillis = excluded.timeMillis;
177
- `.pipe(_effect.Effect.mapError(_RepositoryError.RepositoryWriteError.from('insertTestResults')));
114
+ timeMillis = excluded.timeMillis,
115
+ tags = excluded.tags;
116
+ `;
117
+ }));
178
118
  }
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
197
- });
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);
119
+ });
120
+ 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
+ }
124
+ 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
+ })];
218
153
  }));
219
154
  };
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;
227
- });
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);
237
- });
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')));
243
- });
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')));
249
- });
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')));
155
+ const getLatestTestRun = _effect.Effect.fnUntraced(function* ({
156
+ name,
157
+ variant
158
+ }) {
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
+ `));
255
164
  });
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)));
165
+ const getBaselineTestRun = _effect.Effect.fnUntraced(function* ({
166
+ name,
167
+ variant
168
+ }) {
169
+ 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
+ `));
266
174
  });
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')));
175
+ 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 *;
182
+ `);
288
183
  return yield* _effect.Effect.fromOption(_effect.Array.head(inserted)).pipe(_effect.Effect.mapError(() => new _RepositoryError.TestRunNotCreated({
289
- testSuiteName: name
184
+ testSuiteName: input.name
290
185
  })));
291
- })).pipe(_effect.Effect.catchTag('SqlError', e => _effect.Effect.fail(_RepositoryError.RepositoryQueryError.fromSql('getOrCreateCurrentTestRun')(e))));
292
- const clearStale = ({
293
- 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;
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
+ });
306
196
  }
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;
197
+ }, sql.withTransaction);
198
+ const withTestRun = (input, execute) => _effect.Effect.acquireUseRelease(createTestRun(input), run => _effect.Effect.suspend(() => execute(run)).pipe(_effect.Effect.map(value => ({
199
+ run,
200
+ value
201
+ }))), (run, exit) => finishTestRun(run, _effect.Exit.isSuccess(exit) ? 'Complete' : 'Failed')).pipe(_effect.Effect.map(({
202
+ run,
203
+ value
204
+ }) => ({
205
+ run: new _TestRun.TestRun({
206
+ id: run.id,
207
+ name: run.name,
208
+ variant: run.variant,
209
+ state: 'Complete'
210
+ }),
211
+ value
212
+ })));
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
218
+ });
219
+ }
220
+ const run = found.value;
221
+ if (run.state !== 'Complete') {
222
+ return yield* new _RepositoryError.TestRunIncomplete({
223
+ testSuiteName: run.name,
224
+ testRunId: run.id,
225
+ variant: run.variant
226
+ });
338
227
  }
339
228
  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* ({
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);
238
+ const listTestRuns = _effect.Effect.fnUntraced(function* ({
351
239
  name,
352
- hash
240
+ variant,
241
+ before,
242
+ limit
353
243
  }) {
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.');
244
+ 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
252
+ WHERE run.name = ${name}
253
+ AND ${variant === undefined ? sql`1 = 1` : sql`run.variant = ${variant}`}
254
+ AND ${before === undefined ? sql`1 = 1` : sql`run.id < ${before}`}
255
+ ORDER BY run.id DESC LIMIT ${parsedLimit};
256
+ `);
257
+ return runs.map(({
258
+ isBaseline,
259
+ ...run
260
+ }) => Object.assign(new _TestRun.TestRun(run), {
261
+ isBaseline: isBaseline === 1
262
+ }));
263
+ });
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(({
277
+ variant,
278
+ id,
279
+ name,
280
+ state
281
+ }) => ({
282
+ variant,
283
+ baseline: id === null || name === null || state === null ? _effect.Option.none() : _effect.Option.some(new _TestRun.TestRun({
284
+ id,
285
+ name,
286
+ variant,
287
+ state
288
+ }))
289
+ }));
382
290
  });
383
- const service = {
384
- clearStale,
385
- clearUncommitedTestResults,
386
- commitCurrentTestRun,
387
- getAllTestResults,
388
- getAllTestRuns,
389
- getOrCreateCurrentTestRun,
390
- getLastTestRunHash,
391
- getPreviousTestRun,
291
+ return {
292
+ commitTestRun,
293
+ getTestRun,
294
+ getLatestTestRun,
295
+ getBaselineTestRun,
296
+ listTestRuns,
297
+ listVariantBaselines,
392
298
  getTestResultsStream,
393
- hasResults,
394
- insertTestResult,
395
- insertTestResults,
396
- getAllTestRunResults
299
+ withTestRun,
300
+ withStats: use => (0, _testResultStats.withStats)(use).pipe(_effect.Effect.provideService(_sql.SqlClient.SqlClient, sql)),
301
+ insertTestResult: (result, run, schemas) => insertTestResults([result], run, schemas),
302
+ insertTestResults
397
303
  };
398
- return service;
399
304
  });
400
305
  //# sourceMappingURL=Test.repository.sqlite.js.map