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