@hot-updater/test-utils 1.0.0-rc.17 → 1.0.0-rc.21
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.
- package/README.md +93 -14
- package/dist/index.d.mts +176 -145
- package/dist/index.mjs +1690 -60
- package/dist/node.cjs +210 -0
- package/dist/node.d.cts +49 -4
- package/dist/node.d.mts +49 -4
- package/dist/node.mjs +203 -1
- package/dist/react-native.cjs +177 -0
- package/dist/react-native.d.cts +104 -0
- package/dist/react-native.d.mts +104 -0
- package/dist/react-native.mjs +174 -0
- package/package.json +44 -6
package/README.md
CHANGED
|
@@ -1,40 +1,49 @@
|
|
|
1
1
|
# @hot-updater/test-utils
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Test suites and helpers for Hot Updater storage adapters, database adapters,
|
|
4
|
+
providers, server plugins, client plugins and servers. The official adapters,
|
|
5
|
+
providers, plugins and example servers run these same suites.
|
|
5
6
|
|
|
6
7
|
```bash
|
|
7
8
|
pnpm add -D @hot-updater/test-utils @hot-updater/server vitest
|
|
8
9
|
```
|
|
9
10
|
|
|
11
|
+
It uses the other Hot Updater packages only through their public API, and
|
|
12
|
+
takes them as peer dependencies. `@hot-updater/server` and
|
|
13
|
+
`@hot-updater/plugin-insights` are optional: the database suites load the
|
|
14
|
+
server when they run, and the Insights suites use the plugin's types. The SQL
|
|
15
|
+
test executors of `@hot-updater/test-utils/node` take `pg`, `mysql2` or
|
|
16
|
+
`@electric-sql/pglite`, whichever you test on.
|
|
17
|
+
|
|
10
18
|
## Database adapters and providers
|
|
11
19
|
|
|
12
20
|
Register three suites against a disposable test database:
|
|
13
21
|
|
|
14
|
-
- `setupDatabaseAdapterConformanceSuite` runs the
|
|
22
|
+
- `setupDatabaseAdapterConformanceSuite` runs the database adapter contract case
|
|
15
23
|
by case: value round-trips, ordering, guards, all-or-nothing writes, unique
|
|
16
24
|
and multi-valued indexes, paging under capped native pages, and concurrent
|
|
17
25
|
writers.
|
|
18
26
|
- `setupReadBudgetTestSuite` checks what each read of core and the built-in
|
|
19
|
-
plugins costs at the adapter,
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
plugins costs at the adapter, on `createMeasuredDatabase`: `createHotUpdater`
|
|
28
|
+
over `meterReads({ name, adapter: verifyAdapter(adapter) })` from
|
|
29
|
+
`@hot-updater/plugin-core`. `postgresRowsExamined` and `mysqlRowsExamined`
|
|
30
|
+
also count the rows a SQL database examined.
|
|
22
31
|
- `setupDatabaseTestSuite` runs core, Bundles and the Release Catalog contract
|
|
23
32
|
through the admin and client HTTP APIs of `createHotUpdater`, then the suites
|
|
24
33
|
of the server plugins listed in `plugins`, such as the Insights plugin's
|
|
25
|
-
`insightsTestSuite()
|
|
34
|
+
`insightsTestSuite()`.
|
|
26
35
|
|
|
27
36
|
Connect your database's lifecycle and the real server:
|
|
28
37
|
|
|
29
38
|
```ts
|
|
30
39
|
import { createHotUpdater } from "@hot-updater/server";
|
|
31
|
-
import { createDatabasePluginApis } from "@hot-updater/server/db";
|
|
32
40
|
import {
|
|
33
41
|
createInsightsModel,
|
|
34
42
|
insights,
|
|
35
43
|
} from "@hot-updater/server/plugins/insights";
|
|
36
|
-
|
|
44
|
+
|
|
37
45
|
import {
|
|
46
|
+
insightsTestSuite,
|
|
38
47
|
setupDatabaseTestSuite,
|
|
39
48
|
startHttpTestServer,
|
|
40
49
|
} from "@hot-updater/test-utils";
|
|
@@ -63,9 +72,14 @@ setupDatabaseTestSuite({
|
|
|
63
72
|
// The server above runs insights(), so its suite runs too.
|
|
64
73
|
plugins: [
|
|
65
74
|
insightsTestSuite({
|
|
75
|
+
// The plugin's API on the database, as a server running it assembles it.
|
|
66
76
|
createModel: (database) =>
|
|
67
77
|
createInsightsModel(
|
|
68
|
-
|
|
78
|
+
createHotUpdater({
|
|
79
|
+
database,
|
|
80
|
+
plugins: [insights()],
|
|
81
|
+
clientAccess: "public",
|
|
82
|
+
}).api.insights,
|
|
69
83
|
),
|
|
70
84
|
}),
|
|
71
85
|
],
|
|
@@ -80,15 +94,80 @@ client selector and keep a simulated device's state across updates, rollbacks
|
|
|
80
94
|
and the built-in fallback. Native download, activation and restart remain
|
|
81
95
|
device end-to-end responsibilities.
|
|
82
96
|
|
|
97
|
+
## Storage adapters
|
|
98
|
+
|
|
99
|
+
`setupStorageAdapterTestSuite` runs the storage adapter contract that deploy,
|
|
100
|
+
patch, the Console, the server and `storage prune` rely on: the canonical URI
|
|
101
|
+
of each key below the base path, streamed bodies, failed uploads that leave no
|
|
102
|
+
object, exact deletes, URIs of another bucket or protocol, download URLs, and
|
|
103
|
+
listing. A storage adapter's specs don't need `@hot-updater/server`.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { setupStorageAdapterTestSuite } from "@hot-updater/test-utils";
|
|
107
|
+
|
|
108
|
+
import { myStorage } from "./myStorage";
|
|
109
|
+
|
|
110
|
+
setupStorageAdapterTestSuite({
|
|
111
|
+
name: "myStorage",
|
|
112
|
+
createStorage: async () => ({
|
|
113
|
+
storage: myStorage({ bucket: "bundles", basePath: "ota" }),
|
|
114
|
+
basePath: "ota",
|
|
115
|
+
}),
|
|
116
|
+
operations: ["put", "get", "getDownloadUrl", "exists", "delete"],
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`createStorage` returns the adapter over an empty bucket or base path for each
|
|
121
|
+
case, and the `cleanup` it may return runs after the case. `operations` lists
|
|
122
|
+
what the adapter implements: the suite fails when one of them is missing and
|
|
123
|
+
skips the cases of the others.
|
|
124
|
+
|
|
83
125
|
## Plugins and servers
|
|
84
126
|
|
|
85
|
-
- `createPluginTestHarness` runs one server plugin
|
|
86
|
-
|
|
127
|
+
- `createPluginTestHarness` runs one server plugin on `createEngine` from
|
|
128
|
+
`@hot-updater/plugin-core`, the engine `createHotUpdater` runs plugins on, on
|
|
129
|
+
a memory adapter by default, and measures what each API call reads.
|
|
87
130
|
- `setupReleaseCatalogTestSuite` with `createHttpTestClient` tests a running
|
|
88
131
|
server by URL. Use `createHandlerHttpTestClient` inside a Workers Vitest pool.
|
|
89
132
|
- Server processes can import `createReleaseCatalogTestStorage` from
|
|
90
133
|
`@hot-updater/test-utils/node` to install the suites' storage fixtures.
|
|
91
134
|
|
|
135
|
+
## Client plugins
|
|
136
|
+
|
|
137
|
+
`@hot-updater/test-utils/react-native` runs client plugins the way the app
|
|
138
|
+
does. `setupClientPlugin(plugin, options)` and
|
|
139
|
+
`setupClientPlugins(plugins, options)` set plugins up as `HotUpdater.init`
|
|
140
|
+
does, on the plugin host from `@hot-updater/protocol`, the one the SDK runs,
|
|
141
|
+
against an in-memory device and a server the test answers. The entry loads
|
|
142
|
+
neither React Native nor Vitest, so plugin specs run in plain Node under
|
|
143
|
+
Vitest or Jest when the plugin imports `defineClientPlugin` from
|
|
144
|
+
`@hot-updater/protocol`; the SDK's root entry loads React Native.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { setupClientPlugin } from "@hot-updater/test-utils/react-native";
|
|
148
|
+
|
|
149
|
+
const runtime = setupClientPlugin(myPlugin(), {
|
|
150
|
+
requestHeaders: { "x-api-key": "client-key" },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
runtime.hooks.onAppReady(launch);
|
|
154
|
+
await runtime.settled();
|
|
155
|
+
|
|
156
|
+
expect(runtime.requests[0]?.json()).toEqual(expectedEvent);
|
|
157
|
+
expect(runtime.errors).toEqual([]);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`hooks` calls each hook as the SDK does, without waiting for it, and
|
|
161
|
+
`settled()` waits for what the hooks started. `requests` records what plugins
|
|
162
|
+
sent with `context.fetch`, and the `respond` option answers each request,
|
|
163
|
+
`204 No Content` by default. `errors` holds what the SDK reports to `onError`.
|
|
164
|
+
`storage` is the device's plugin storage: pass it to the next setup to start
|
|
165
|
+
the plugins again on the same device, or seed one with `createTestStorage()`.
|
|
166
|
+
|
|
92
167
|
See the
|
|
93
|
-
[
|
|
94
|
-
|
|
168
|
+
[storage adapter guide](https://hot-updater.dev/docs/storage-adapters/custom-storage#test-the-adapter),
|
|
169
|
+
the
|
|
170
|
+
[database adapter guide](https://hot-updater.dev/docs/database-adapters/custom-database),
|
|
171
|
+
and the
|
|
172
|
+
[client plugin guide](https://hot-updater.dev/docs/react-native-api/client-plugins#test-the-plugin)
|
|
173
|
+
for the contracts and complete specs.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ArtifactInfo, Bundle
|
|
2
|
-
import { DatabaseAdapter,
|
|
3
|
-
import {
|
|
1
|
+
import { ArtifactInfo, Bundle } from "@hot-updater/protocol";
|
|
2
|
+
import { AnyHotUpdaterPlugin, BundlePatchRow, BundleRow, ChannelRow, ClientAuth, DatabaseAdapter, DeployReleasePolicy, EngineDatabase, HotUpdaterCoreApi, HotUpdaterDatabase, KeyValueStore, ModuleSchema, PhysicalTable, PluginApis, ReadMeasurement, ReleaseRow, StorageAdapter, StorageOperation } from "@hot-updater/plugin-core";
|
|
3
|
+
import { BundleEventRow, InsightsModel } from "@hot-updater/plugin-insights/server";
|
|
4
4
|
//#region src/httpTestClient.d.ts
|
|
5
5
|
type HttpTestRequestInit = NonNullable<ConstructorParameters<typeof Request>[1]>;
|
|
6
6
|
type HttpTestRequest = (path: string, init?: HttpTestRequestInit) => Promise<Response>;
|
|
@@ -128,7 +128,7 @@ type DatabaseTestSuiteOptions<TDatabase extends EngineDatabase = EngineDatabase>
|
|
|
128
128
|
*/
|
|
129
129
|
readonly createHttpClient: (options: {
|
|
130
130
|
readonly database: TDatabase;
|
|
131
|
-
readonly storage: readonly
|
|
131
|
+
readonly storage: readonly StorageAdapter[];
|
|
132
132
|
}) => HttpTestServer | Promise<HttpTestServer>;
|
|
133
133
|
/** The suites of the server plugins the provider opts in to test. */
|
|
134
134
|
readonly plugins?: readonly DatabasePluginTestSuite<TDatabase>[];
|
|
@@ -175,9 +175,38 @@ declare const conformanceExpiring: PhysicalTable;
|
|
|
175
175
|
*/
|
|
176
176
|
declare const setupDatabaseAdapterConformanceSuite: (options: DatabaseAdapterConformanceOptions) => void;
|
|
177
177
|
//#endregion
|
|
178
|
+
//#region src/setupStorageAdapterTestSuite.d.ts
|
|
179
|
+
interface StorageAdapterTestSuiteOptions {
|
|
180
|
+
readonly name: string;
|
|
181
|
+
/**
|
|
182
|
+
* Returns the adapter over an empty bucket or base path. Every case creates
|
|
183
|
+
* its own and calls `cleanup` after it.
|
|
184
|
+
*/
|
|
185
|
+
readonly createStorage: () => Promise<{
|
|
186
|
+
readonly storage: StorageAdapter;
|
|
187
|
+
/**
|
|
188
|
+
* The base path the adapter stores keys below. With it, every URI `put`
|
|
189
|
+
* returns must have the key `<basePath>/<key>`.
|
|
190
|
+
*/
|
|
191
|
+
readonly basePath?: string;
|
|
192
|
+
readonly cleanup?: () => Promise<void>;
|
|
193
|
+
}>;
|
|
194
|
+
/**
|
|
195
|
+
* The operations the adapter implements. The suite requires them, with
|
|
196
|
+
* `put` and `get`, and skips the cases of every other operation. Without
|
|
197
|
+
* it, the suite skips the cases of the operations the adapter lacks.
|
|
198
|
+
*/
|
|
199
|
+
readonly operations?: readonly StorageOperation[];
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The storage adapter contract that deploy, patch, the Console, the server,
|
|
203
|
+
* and `storage prune` rely on. Every official storage adapter runs it.
|
|
204
|
+
*/
|
|
205
|
+
declare const setupStorageAdapterTestSuite: (options: StorageAdapterTestSuiteOptions) => void;
|
|
206
|
+
//#endregion
|
|
178
207
|
//#region src/sqlRowsExamined.d.ts
|
|
179
208
|
/**
|
|
180
|
-
* Rows a SQL database examined for the reads a
|
|
209
|
+
* Rows a SQL database examined for the reads a database adapter made: what
|
|
181
210
|
* read-budget suites compare with the rows the adapter read.
|
|
182
211
|
*/
|
|
183
212
|
interface RowsExamined {
|
|
@@ -237,72 +266,17 @@ declare const mysqlRowsExamined: (query: SqlQuery) => {
|
|
|
237
266
|
};
|
|
238
267
|
//#endregion
|
|
239
268
|
//#region src/setupReadBudgetTestSuite.d.ts
|
|
240
|
-
/** Core's API with the client routes' reads, which the public API leaves out. */
|
|
241
|
-
type ReadBudgetCore = HotUpdaterCoreApi & {
|
|
242
|
-
getReleaseCatalog(input: {
|
|
243
|
-
readonly strategy: "APP_VERSION";
|
|
244
|
-
readonly platform: "ios" | "android";
|
|
245
|
-
readonly channelKey: string;
|
|
246
|
-
readonly appVersion: string;
|
|
247
|
-
}): Promise<ReleaseCatalog | null>;
|
|
248
|
-
getArtifactInfo(targetBundleId: string, currentBundleId: string, artifactProtocolVersion: 1): Promise<ArtifactInfo | null>;
|
|
249
|
-
latestReleaseId(scopeKey: string): Promise<string | null>;
|
|
250
|
-
};
|
|
251
|
-
/** How core reads bundle manifests and resolves file URLs. */
|
|
252
|
-
interface ReadBudgetStorage {
|
|
253
|
-
readonly readStorageText?: (storageUri: string) => Promise<string | null>;
|
|
254
|
-
readonly resolveFileUrl: (storageUri: string | null) => Promise<string | null>;
|
|
255
|
-
}
|
|
256
|
-
/** What `createMeasuredDatabase` returns: core and the plugins on one engine in verify mode. */
|
|
257
|
-
interface ReadBudgetDatabase {
|
|
258
|
-
readonly core: ReadBudgetCore;
|
|
259
|
-
readonly api: Readonly<Record<string, unknown>>;
|
|
260
|
-
readonly clientAuth?: {
|
|
261
|
-
authenticate(headers: Headers): Promise<boolean>;
|
|
262
|
-
};
|
|
263
|
-
measureReads<T>(read: () => Promise<T>): Promise<{
|
|
264
|
-
readonly result: T;
|
|
265
|
-
readonly adapter: DatabaseReadCount;
|
|
266
|
-
readonly engine: {
|
|
267
|
-
readonly calls: number;
|
|
268
|
-
readonly rows: number;
|
|
269
|
-
};
|
|
270
|
-
}>;
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* What the suite takes from `@hot-updater/server`; pass those exports, which
|
|
274
|
-
* keeps test-utils free of a dependency on the server.
|
|
275
|
-
*/
|
|
276
|
-
interface ReadBudgetServer {
|
|
277
|
-
/** `createMeasuredDatabase` from `@hot-updater/server/db`. */
|
|
278
|
-
createMeasuredDatabase(adapter: DatabaseAdapter, plugins: readonly unknown[], options: {
|
|
279
|
-
readonly now: () => number;
|
|
280
|
-
readonly storage: ReadBudgetStorage;
|
|
281
|
-
}): ReadBudgetDatabase;
|
|
282
|
-
/** `toolingTargetOf` from `@hot-updater/server/database`: core's and the plugins' tables to create. */
|
|
283
|
-
toolingTargetOf(plugins: readonly unknown[]): {
|
|
284
|
-
readonly schema: {
|
|
285
|
-
readonly tables: readonly PhysicalTable[];
|
|
286
|
-
};
|
|
287
|
-
};
|
|
288
|
-
/** `[insights(), apiKeys()]`, the plugins whose reads have budgets. */
|
|
289
|
-
readonly plugins: readonly unknown[];
|
|
290
|
-
/** `targetBaseCandidateKey` from `@hot-updater/server/db`. */
|
|
291
|
-
targetBaseCandidateKey(target: {
|
|
292
|
-
readonly channel: string;
|
|
293
|
-
readonly platform: "ios" | "android";
|
|
294
|
-
readonly fingerprintHash: string | null;
|
|
295
|
-
readonly appVersion: string | null;
|
|
296
|
-
}): string | null;
|
|
297
|
-
}
|
|
298
269
|
interface ReadBudgetAdapterContext {
|
|
270
|
+
/**
|
|
271
|
+
* Core's tables, those of the plugins the suite measures, and the settings
|
|
272
|
+
* table, where the retention passes keep their lease.
|
|
273
|
+
*/
|
|
299
274
|
readonly tables: readonly PhysicalTable[];
|
|
300
275
|
/** Test-only: the adapter must fetch at most this many rows per native page. */
|
|
301
276
|
readonly nativePageSize: number;
|
|
302
277
|
}
|
|
303
278
|
interface ReadBudgetSuiteOptions {
|
|
304
279
|
readonly name: string;
|
|
305
|
-
readonly server: ReadBudgetServer;
|
|
306
280
|
/** Returns an adapter over freshly created, empty tables. */
|
|
307
281
|
readonly createAdapter: (context: ReadBudgetAdapterContext) => Promise<{
|
|
308
282
|
readonly adapter: DatabaseAdapter;
|
|
@@ -317,44 +291,18 @@ interface ReadBudgetSuiteOptions {
|
|
|
317
291
|
}>;
|
|
318
292
|
}
|
|
319
293
|
/**
|
|
320
|
-
* The read-budget suite (PRD S4) on one backend: core and the
|
|
321
|
-
* plugins
|
|
322
|
-
* two rows. Every API in the read-budget list reads
|
|
323
|
-
* adapter and at the engine; with `examined`, the
|
|
324
|
-
* rows than the adapter read, within the backend's
|
|
294
|
+
* The read-budget suite (PRD S4) on one backend: core and the server's
|
|
295
|
+
* built-in plugins on `createMeasuredDatabase` over the backend's adapter,
|
|
296
|
+
* native pages capped at two rows. Every API in the read-budget list reads
|
|
297
|
+
* exactly its budget at the adapter and at the engine; with `examined`, the
|
|
298
|
+
* database examines no more rows than the adapter read, within the backend's
|
|
299
|
+
* native multipliers. It loads `@hot-updater/server`, an optional peer.
|
|
325
300
|
*/
|
|
326
301
|
declare const setupReadBudgetTestSuite: (options: ReadBudgetSuiteOptions) => void;
|
|
327
302
|
//#endregion
|
|
328
303
|
//#region src/setupAggregateBatchingTestSuite.d.ts
|
|
329
|
-
interface Module {
|
|
330
|
-
readonly id: string;
|
|
331
|
-
readonly schema: object;
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* What the suite takes from `@hot-updater/server/database`; pass that
|
|
335
|
-
* module, which keeps test-utils free of a dependency on the server.
|
|
336
|
-
*/
|
|
337
|
-
interface AggregateBatchingEngine {
|
|
338
|
-
resolveSchema(modules: readonly Module[]): {
|
|
339
|
-
readonly tables: readonly PhysicalTable[];
|
|
340
|
-
};
|
|
341
|
-
readonly aggregateBatchingModule: Module;
|
|
342
|
-
createDatabaseEngine(options: {
|
|
343
|
-
readonly adapter: DatabaseAdapter;
|
|
344
|
-
readonly schema: never;
|
|
345
|
-
readonly batching?: AggregateBatching;
|
|
346
|
-
readonly retry?: {
|
|
347
|
-
readonly attempts?: number;
|
|
348
|
-
};
|
|
349
|
-
}): {
|
|
350
|
-
database(module: Module): unknown;
|
|
351
|
-
flush(): Promise<void>;
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
304
|
interface AggregateBatchingSuiteOptions {
|
|
355
305
|
readonly name: string;
|
|
356
|
-
/** `import * as engine from "@hot-updater/server/database"`. */
|
|
357
|
-
readonly engine: AggregateBatchingEngine;
|
|
358
306
|
/** Returns an adapter over an empty store; each case calls it once. */
|
|
359
307
|
readonly createAdapter: () => Promise<{
|
|
360
308
|
readonly adapter: DatabaseAdapter;
|
|
@@ -368,7 +316,7 @@ interface AggregateBatchingSuiteOptions {
|
|
|
368
316
|
* writes with each transaction and a compaction merges once, and a memory
|
|
369
317
|
* buffer, read back exactly as transactional writes read.
|
|
370
318
|
*/
|
|
371
|
-
declare const setupAggregateBatchingTestSuite: ({ name,
|
|
319
|
+
declare const setupAggregateBatchingTestSuite: ({ name, createAdapter, oversizedRows }: AggregateBatchingSuiteOptions) => void;
|
|
372
320
|
//#endregion
|
|
373
321
|
//#region src/runContentionHarness.d.ts
|
|
374
322
|
/** Delays every adapter call by `ms`, like a round trip to a remote database. */
|
|
@@ -396,40 +344,48 @@ interface ContentionReport {
|
|
|
396
344
|
*/
|
|
397
345
|
declare const runContentionHarness: (options: ContentionOptions) => Promise<ContentionReport>;
|
|
398
346
|
//#endregion
|
|
399
|
-
//#region src/
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
readonly
|
|
403
|
-
readonly
|
|
347
|
+
//#region src/createMeasuredDatabase.d.ts
|
|
348
|
+
/** How core reads bundle manifests and resolves the file URLs of `storage://` URIs. */
|
|
349
|
+
interface MeasuredDatabaseStorage {
|
|
350
|
+
readonly readStorageText?: (storageUri: string) => Promise<string | null>;
|
|
351
|
+
readonly resolveFileUrl: (storageUri: string) => Promise<string>;
|
|
404
352
|
}
|
|
405
|
-
interface
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
353
|
+
interface MeasuredDatabaseOptions {
|
|
354
|
+
/**
|
|
355
|
+
* The plugins' clock, which they read through `ctx.now`. Core's
|
|
356
|
+
* timestamps and the retention passes run on `Date.now`.
|
|
357
|
+
*/
|
|
358
|
+
readonly now?: () => number;
|
|
359
|
+
/**
|
|
360
|
+
* The storage of `storage://` URIs, for reads that resolve them, such as
|
|
361
|
+
* artifact resolution; without it those reads fail.
|
|
362
|
+
*/
|
|
363
|
+
readonly storage?: MeasuredDatabaseStorage;
|
|
364
|
+
}
|
|
365
|
+
/** Core and the plugins' APIs on `createHotUpdater`, with the database's read meter. */
|
|
366
|
+
interface MeasuredDatabase<TApi = Readonly<Record<string, unknown>>> {
|
|
367
|
+
readonly core: HotUpdaterCoreApi;
|
|
368
|
+
readonly api: TApi;
|
|
369
|
+
/** The client-route policy, when a plugin provides one. */
|
|
370
|
+
readonly clientAuth?: ClientAuth;
|
|
371
|
+
/** Runs `read` and reports what it read at the adapter and at the engine. */
|
|
372
|
+
measureReads<T>(read: () => Promise<T>): Promise<ReadMeasurement<T>>;
|
|
415
373
|
}
|
|
416
374
|
/**
|
|
417
|
-
*
|
|
418
|
-
*
|
|
375
|
+
* Core and the plugins over `adapter` as `createHotUpdater` runs them: what
|
|
376
|
+
* read-budget suites measure. The database is `meterReads` over
|
|
377
|
+
* `verifyAdapter(adapter)`, so every adapter call is checked against the
|
|
378
|
+
* contract and `measureReads` reports each call's reads; the adapter runs
|
|
379
|
+
* as given, without the schema fence. It loads `@hot-updater/server`, an
|
|
380
|
+
* optional peer, when called.
|
|
419
381
|
*/
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
};
|
|
424
|
-
createDatabaseEngine(options: {
|
|
425
|
-
readonly adapter: DatabaseAdapter;
|
|
426
|
-
readonly schema: never;
|
|
427
|
-
readonly verify?: boolean;
|
|
428
|
-
}): HarnessEngine;
|
|
429
|
-
}
|
|
382
|
+
declare const createMeasuredDatabase: <const TPlugins extends readonly AnyHotUpdaterPlugin[]>(adapter: DatabaseAdapter, plugins: TPlugins, options?: MeasuredDatabaseOptions) => Promise<MeasuredDatabase<PluginApis<TPlugins>>>;
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/createPluginTestHarness.d.ts
|
|
430
385
|
interface HarnessPlugin {
|
|
431
386
|
readonly id: string;
|
|
432
|
-
readonly
|
|
387
|
+
readonly schemaVersion: string;
|
|
388
|
+
readonly schema: ModuleSchema;
|
|
433
389
|
/** `false` keeps the declared table names, as `createHotUpdater` does. */
|
|
434
390
|
readonly namespace?: false;
|
|
435
391
|
init(context: never): {
|
|
@@ -437,36 +393,111 @@ interface HarnessPlugin {
|
|
|
437
393
|
};
|
|
438
394
|
}
|
|
439
395
|
interface PluginTestHarnessOptions {
|
|
440
|
-
/** `import * as engine from "@hot-updater/server/database"`. */
|
|
441
|
-
readonly engine: PluginTestEngine;
|
|
442
396
|
/** Defaults to a fresh memory adapter. */
|
|
443
397
|
readonly adapter?: DatabaseAdapter;
|
|
444
398
|
/** The plugin's clock; `setNow` changes it later. */
|
|
445
399
|
readonly now?: () => number;
|
|
446
400
|
}
|
|
447
401
|
/**
|
|
448
|
-
* Runs one plugin the
|
|
449
|
-
*
|
|
402
|
+
* Runs one plugin on `createEngine`, the storage engine `createHotUpdater`
|
|
403
|
+
* runs its plugins on, so tests can call its API and measure what each call
|
|
404
|
+
* reads. Every adapter call is checked against the adapter contract
|
|
405
|
+
* (`verifyAdapter`), and `ctx.core` is empty: core's reads need a server.
|
|
406
|
+
* The engine gets the adapter without `prune`, as a store that expires rows
|
|
407
|
+
* itself would give it, so no retention pass adds to what a call reads and
|
|
408
|
+
* writes; rows stay past their retention.
|
|
450
409
|
*/
|
|
451
|
-
declare const createPluginTestHarness: <TPlugin extends HarnessPlugin>(plugin: TPlugin, options
|
|
410
|
+
declare const createPluginTestHarness: <TPlugin extends HarnessPlugin>(plugin: TPlugin, options?: PluginTestHarnessOptions) => Promise<{
|
|
452
411
|
api: ReturnType<TPlugin["init"]>["api"];
|
|
453
412
|
instance: ReturnType<TPlugin["init"]>;
|
|
454
|
-
/** The plugin's own database handle
|
|
455
|
-
db:
|
|
413
|
+
/** The plugin's own database handle. */
|
|
414
|
+
db: HotUpdaterDatabase<TPlugin["schema"]>;
|
|
456
415
|
adapter: DatabaseAdapter;
|
|
457
|
-
tables
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
readonly engine: {
|
|
463
|
-
readonly calls: number;
|
|
464
|
-
readonly rows: number;
|
|
465
|
-
};
|
|
466
|
-
}>;
|
|
467
|
-
setNow: (now: () => number) => void;
|
|
416
|
+
/** The plugin's physical tables. */
|
|
417
|
+
tables: import("@hot-updater/plugin-core").PhysicalTable[];
|
|
418
|
+
/** Reads at the adapter and at the engine while `read` runs. */
|
|
419
|
+
measureReads: <T>(read: () => Promise<T>) => Promise<import("@hot-updater/plugin-core").ReadMeasurement<T>>;
|
|
420
|
+
setNow: (next: () => number) => void;
|
|
468
421
|
}>;
|
|
469
422
|
//#endregion
|
|
423
|
+
//#region src/databaseTestFixtures.d.ts
|
|
424
|
+
declare const createChannelRowFixture: (name?: string) => ChannelRow;
|
|
425
|
+
declare const createBundleRowFixture: (suffix: string, _channel?: string) => BundleRow;
|
|
426
|
+
declare const createBundlePatchRowFixture: (suffix: string, bundleId: string, baseBundleId: string, orderIndex?: number) => BundlePatchRow;
|
|
427
|
+
declare const createReleaseRowFixture: (suffix: string, bundle: BundleRow, channel: ChannelRow) => ReleaseRow;
|
|
428
|
+
declare const createBundleFixture: (suffix: string, _channel?: string) => Bundle;
|
|
429
|
+
//#endregion
|
|
430
|
+
//#region src/storeBundles.d.ts
|
|
431
|
+
/**
|
|
432
|
+
* Stores bundles and their patches with no release, as a database holds an
|
|
433
|
+
* artifact that no release uses. `deploy` always adds a release, and
|
|
434
|
+
* deleting a bundle's last release deletes the bundle, so tests store such
|
|
435
|
+
* bundles through the engine. A patch's base comes before it in `bundles`.
|
|
436
|
+
*/
|
|
437
|
+
declare const storeBundles: (database: EngineDatabase, bundles: readonly Bundle[]) => Promise<void>;
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/insights/expectInsightsIndex.d.ts
|
|
440
|
+
/** Allow committed secondary indexes to catch up; query failures still fail. */
|
|
441
|
+
declare const expectInsightsIndex: <T>(query: () => Promise<T>, expected: T) => Promise<void>;
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/insights/fixtures.d.ts
|
|
444
|
+
/** An applied update from install `suffix`, received at `receivedAtMs`. */
|
|
445
|
+
declare const createBundleEventRowFixture: (suffix: string, receivedAtMs: number) => Extract<BundleEventRow, {
|
|
446
|
+
type: "UPDATE_APPLIED" | "RECOVERED";
|
|
447
|
+
}>;
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/insights/setupInsightsHttpTestSuite.d.ts
|
|
450
|
+
/**
|
|
451
|
+
* The Insights plugin's routes on a database: a client reports an event, and
|
|
452
|
+
* the admin routes read it back. The server must run `insights()`.
|
|
453
|
+
*/
|
|
454
|
+
declare const setupInsightsHttpTestSuite: (options: {
|
|
455
|
+
readonly getClient: () => HttpTestClient;
|
|
456
|
+
}) => void;
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/insights/setupInsightsModelTestSuite.d.ts
|
|
459
|
+
/**
|
|
460
|
+
* The Insights report contract on one database: pass the Insights plugin's
|
|
461
|
+
* model over it, `createInsightsModel(createHotUpdater({ database, plugins:
|
|
462
|
+
* [insights()], clientAccess: "public" }).api.insights)`, as `createDatabase`.
|
|
463
|
+
*/
|
|
464
|
+
declare const setupInsightsModelTestSuite: (lifecycle: DatabaseTestLifecycle<InsightsModel>) => void;
|
|
465
|
+
//#endregion
|
|
466
|
+
//#region src/insights/index.d.ts
|
|
467
|
+
/**
|
|
468
|
+
* The Insights plugin's tests, for `setupDatabaseTestSuite`'s `plugins`: its
|
|
469
|
+
* HTTP routes on the provider's server, which must run `insights()`, and
|
|
470
|
+
* with `createModel` its report contract on the provider's database, such as
|
|
471
|
+
* `(database) => createInsightsModel(createHotUpdater({ database, plugins:
|
|
472
|
+
* [insights()], clientAccess: "public" }).api.insights)`.
|
|
473
|
+
*/
|
|
474
|
+
declare const insightsTestSuite: <TDatabase = EngineDatabase>(options?: {
|
|
475
|
+
readonly createModel?: (database: TDatabase) => InsightsModel;
|
|
476
|
+
}) => {
|
|
477
|
+
name: string;
|
|
478
|
+
register: (context: {
|
|
479
|
+
readonly getClient: () => HttpTestClient;
|
|
480
|
+
readonly getDatabase: () => TDatabase;
|
|
481
|
+
}) => void;
|
|
482
|
+
};
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/kvTestStore.d.ts
|
|
485
|
+
interface MemoryKeyValueStoreOptions {
|
|
486
|
+
/** Test-only: the most items one `query` page returns. */
|
|
487
|
+
readonly nativePageSize?: number;
|
|
488
|
+
readonly limits?: KeyValueStore["limits"];
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* An in-memory key-value store that behaves like DynamoDB where the adapter
|
|
492
|
+
* can tell: a write checks every condition against the state before it,
|
|
493
|
+
* refuses two ops on one item, and applies all ops or none.
|
|
494
|
+
*/
|
|
495
|
+
declare const createMemoryKeyValueStore: (options?: MemoryKeyValueStoreOptions) => KeyValueStore & {
|
|
496
|
+
readonly items: () => number;
|
|
497
|
+
/** Each item's expiry, as a store's native TTL would hold it, by `[pk, sk]` JSON. */
|
|
498
|
+
readonly expiries: () => ReadonlyMap<string, number>;
|
|
499
|
+
};
|
|
500
|
+
//#endregion
|
|
470
501
|
//#region src/setupReleaseCatalogTestSuite.d.ts
|
|
471
502
|
/**
|
|
472
503
|
* Server conformance through HTTP only, on admin API protocol 2. The caller
|
|
@@ -494,6 +525,6 @@ declare const releaseCatalogArtifact: (bundle: BundleRow) => ArtifactInfo;
|
|
|
494
525
|
/** Install these storage objects in the test server, independently of its DB. */
|
|
495
526
|
declare const RELEASE_CATALOG_STORAGE_FIXTURES: Readonly<Record<string, string>>;
|
|
496
527
|
/** Storage boundary shared by provider and separately spawned server tests. */
|
|
497
|
-
declare const createReleaseCatalogTestStorage: () =>
|
|
528
|
+
declare const createReleaseCatalogTestStorage: () => StorageAdapter;
|
|
498
529
|
//#endregion
|
|
499
|
-
export { AdminApiTestClient,
|
|
530
|
+
export { AdminApiTestClient, AggregateBatchingSuiteOptions, ContentionOptions, ContentionReport, DatabaseAdapterConformanceContext, DatabaseAdapterConformanceOptions, DatabasePluginTestSuite, type DatabaseTestLifecycle, DatabaseTestSuiteOptions, HttpTestClient, HttpTestHandlers, HttpTestRequest, HttpTestRequestInit, HttpTestServer, MeasuredDatabase, MeasuredDatabaseOptions, MeasuredDatabaseStorage, MemoryKeyValueStoreOptions, PluginTestHarnessOptions, RELEASE_CATALOG_DOWNLOAD_HASH, RELEASE_CATALOG_MANIFEST_URIS, RELEASE_CATALOG_STORAGE_FIXTURES, ReadBudgetAdapterContext, ReadBudgetSuiteOptions, RowsExamined, SqlExecutorLike, SqlQuery, SqlStatementLike, StorageAdapterTestSuiteOptions, conformanceCounters, conformanceExpiring, conformanceItems, createAdminApiTestClient, createBundleEventRowFixture, createBundleFixture, createBundlePatchRowFixture, createBundleRowFixture, createChannelRowFixture, createHandlerHttpTestClient, createHttpTestClient, createMeasuredDatabase, createMemoryKeyValueStore, createPluginTestHarness, createReleaseCatalogTestStorage, createReleaseRowFixture, expectInsightsIndex, insightsTestSuite, jsonRequest, mysqlRowsExamined, postgresRowsExamined, releaseCatalogArtifact, releaseCatalogDownloadUrl, runContentionHarness, setupAggregateBatchingTestSuite, setupBundleMethodsTestSuite, setupCoreAdminTestSuite, setupDatabaseAdapterConformanceSuite, setupDatabaseTestSuite, setupInsightsHttpTestSuite, setupInsightsModelTestSuite, setupReadBudgetTestSuite, setupReleaseCatalogTestSuite, setupSemverSatisfiesTestSuite, setupStorageAdapterTestSuite, startHttpTestServer, storeBundles, toDeployBundle, withAdapterLatency };
|