@stowage/conformance 0.0.0 → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.js +2220 -0
- package/package.json +41 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexander Kaufmann
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @stowage/conformance
|
|
2
|
+
|
|
3
|
+
The cases every stowage adapter has to pass, as plain cases a test framework maps onto its own
|
|
4
|
+
runner. An adapter written outside this repository passes the same cases as the ones inside it.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install --save-dev @stowage/conformance
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Write a target
|
|
13
|
+
|
|
14
|
+
A `ConformanceTarget` tells the suite how to construct the storage under test. `createStorage`
|
|
15
|
+
returns a storage the run may write to below any prefix, constructed from outside the adapter the
|
|
16
|
+
way a caller would construct it
|
|
17
|
+
([spec 9.3](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#93-what-the-target-promises)).
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { memoryStorage } from "@stowage/adapter-memory";
|
|
21
|
+
import type { ConformanceTarget } from "@stowage/conformance";
|
|
22
|
+
|
|
23
|
+
export const target: ConformanceTarget = {
|
|
24
|
+
name: "@stowage/adapter-memory",
|
|
25
|
+
createStorage: () => memoryStorage(),
|
|
26
|
+
};
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The optional members widen the run:
|
|
30
|
+
|
|
31
|
+
- `cleanup(keyPrefix)` runs once at the end, and defaults to `deleteAll(keyPrefix)` on a fresh
|
|
32
|
+
storage. Every key a run writes begins with its prefix, so two runs against one bucket do not
|
|
33
|
+
interfere.
|
|
34
|
+
- `createStorageWithBadCredentials`, `createStorageWithExpiredCredentials` and
|
|
35
|
+
`createStorageWithDeniedCredentials` return storages whose credential the provider refuses, has
|
|
36
|
+
expired, or accepts for reading alone. A case that needs a factory the target leaves out is
|
|
37
|
+
reported `skipped`.
|
|
38
|
+
|
|
39
|
+
## Fill the declaration
|
|
40
|
+
|
|
41
|
+
The suite reads what the storage supports from its `capabilities`, once per run, before the first
|
|
42
|
+
case. The storage lists every name out of `capabilityNames` it implements, each once, and the list
|
|
43
|
+
is fixed when it is constructed
|
|
44
|
+
([spec 4.9](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#49-capabilities)).
|
|
45
|
+
Every storage the target creates in one run declares the same; two configurations are two targets.
|
|
46
|
+
|
|
47
|
+
A case whose `requires` the storage declares runs its `run` half. A case missing a name of its
|
|
48
|
+
`requires` runs the `runWithout` half, which checks what the storage does without the capability:
|
|
49
|
+
most refuse the call with `Unsupported` naming it, `presignedUrls` has `presignGet` and
|
|
50
|
+
`presignPut` absent from the storage, and a few check the weaker behavior, such as a copy whose
|
|
51
|
+
destination reads `{}`
|
|
52
|
+
([spec 9.5](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#95-cases)).
|
|
53
|
+
An adapter refuses such a call like this:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { StorageError } from "@stowage/core";
|
|
57
|
+
|
|
58
|
+
export function refuseUserMetadata(bucket: string): StorageError {
|
|
59
|
+
return new StorageError({
|
|
60
|
+
code: "Unsupported",
|
|
61
|
+
message: "This storage keeps no user metadata",
|
|
62
|
+
operation: "put",
|
|
63
|
+
bucket,
|
|
64
|
+
provider: "my-provider",
|
|
65
|
+
attempts: 0,
|
|
66
|
+
capability: "userMetadata",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`expectUnsupported(call, capability)` asserts that refusal in a test of the adapter's own.
|
|
72
|
+
|
|
73
|
+
## Run the cases
|
|
74
|
+
|
|
75
|
+
`describeConformance(target, framework)` maps every case onto the `describe` and `test` of the
|
|
76
|
+
framework it is handed
|
|
77
|
+
([spec 9.2](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#92-running-the-suite)).
|
|
78
|
+
It runs the `fast` cases, and both tiers with `includeSlow: true`.
|
|
79
|
+
|
|
80
|
+
Vitest:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { memoryStorage } from "@stowage/adapter-memory";
|
|
84
|
+
import { describeConformance } from "@stowage/conformance";
|
|
85
|
+
import { describe, test } from "vitest";
|
|
86
|
+
|
|
87
|
+
describeConformance(
|
|
88
|
+
{ name: "@stowage/adapter-memory", createStorage: () => memoryStorage() },
|
|
89
|
+
{ describe, test, includeSlow: true },
|
|
90
|
+
);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`bun:test`:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { describe, test } from "bun:test";
|
|
97
|
+
|
|
98
|
+
import { memoryStorage } from "@stowage/adapter-memory";
|
|
99
|
+
import { describeConformance } from "@stowage/conformance";
|
|
100
|
+
|
|
101
|
+
describeConformance(
|
|
102
|
+
{ name: "@stowage/adapter-memory", createStorage: () => memoryStorage() },
|
|
103
|
+
{ describe, test },
|
|
104
|
+
);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`Deno.test` has no `describe` beside it, so a block becomes the leading part of each test's name:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { memoryStorage } from "@stowage/adapter-memory";
|
|
111
|
+
import { describeConformance } from "@stowage/conformance";
|
|
112
|
+
|
|
113
|
+
const enclosingNames: string[] = [];
|
|
114
|
+
|
|
115
|
+
describeConformance(
|
|
116
|
+
{ name: "@stowage/adapter-memory", createStorage: () => memoryStorage() },
|
|
117
|
+
{
|
|
118
|
+
describe(name, body) {
|
|
119
|
+
enclosingNames.push(name);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
body();
|
|
123
|
+
} finally {
|
|
124
|
+
enclosingNames.pop();
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
test(name, body) {
|
|
128
|
+
Deno.test([...enclosingNames, name].join(" > "), body);
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Run the cases on `workerd`
|
|
135
|
+
|
|
136
|
+
`workerd` has no test framework. `runAll(target)` runs every case and returns the results, which a
|
|
137
|
+
worker answers with for the process that started it to report:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { memoryStorage } from "@stowage/adapter-memory";
|
|
141
|
+
import { runAll } from "@stowage/conformance";
|
|
142
|
+
|
|
143
|
+
export default {
|
|
144
|
+
async fetch(): Promise<Response> {
|
|
145
|
+
const results = await runAll({
|
|
146
|
+
name: "@stowage/adapter-memory",
|
|
147
|
+
createStorage: () => memoryStorage(),
|
|
148
|
+
});
|
|
149
|
+
const failed = results.filter((result) => result.status === "failed");
|
|
150
|
+
|
|
151
|
+
return Response.json(results, { status: failed.length === 0 ? 200 : 500 });
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
A failed result carries the error as `name`, `message`, `stack` and, for a `StorageError`, `code`.
|
|
157
|
+
|
|
158
|
+
## Read an adapter
|
|
159
|
+
|
|
160
|
+
[`@stowage/adapter-memory`](https://github.com/stowage-js/stowage/tree/@stowage/adapter-memory@0.2.0/packages/adapter-memory/src)
|
|
161
|
+
is the implementation a third-party adapter is read against, out of the four adapters of this
|
|
162
|
+
repository that pass these cases: `@stowage/adapter-memory`, `@stowage/adapter-fs`,
|
|
163
|
+
`@stowage/adapter-s3` and `@stowage/adapter-azure-blob`. It enforces the key rule exactly,
|
|
164
|
+
declares four of the five capabilities, and has no network in the way. The cases themselves are
|
|
165
|
+
written against the behavior of S3, not against it.
|
|
166
|
+
|
|
167
|
+
What the suite leaves to an adapter's own tests, such as flat memory during a large upload or the
|
|
168
|
+
retry curve, is listed in
|
|
169
|
+
[spec 9.4](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#94-what-the-suite-does-not-assert).
|
|
170
|
+
|
|
171
|
+
## Runtimes
|
|
172
|
+
|
|
173
|
+
Node 24 and later, Bun, Deno and `workerd` at the compatibility date `2026-09-01` without Node APIs. CI last ran green on Bun 1.4.2 and Deno 2.9.6.
|
|
174
|
+
|
|
175
|
+
The bundle measures 11.6 kB minified and gzipped, `@stowage/core` included.
|
|
176
|
+
|
|
177
|
+
## Specification
|
|
178
|
+
|
|
179
|
+
[`docs/spec.md` at `@stowage/conformance@0.2.0`](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#9-stowageconformance)
|
|
180
|
+
is the contract: a caller may rely on what it states and on nothing else this package happens to
|
|
181
|
+
export. The [terms it uses](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/CONTEXT.md)
|
|
182
|
+
and the [decisions behind it](https://github.com/stowage-js/stowage/tree/@stowage/conformance@0.2.0/docs/adr)
|
|
183
|
+
are at the same tag.
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { CapabilityName, Storage, StorageErrorCode } from "@stowage/core";
|
|
2
|
+
//#region src/assertions.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Spec 4.9 leaves a call needing a capability the storage does not declare with an
|
|
5
|
+
* `Unsupported` error naming it, and spec 9.2 has most of the `runWithout` halves assert
|
|
6
|
+
* that through this.
|
|
7
|
+
*/
|
|
8
|
+
export declare function expectUnsupported(call: () => Promise<unknown>, capability: CapabilityName): Promise<void>;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/target.d.ts
|
|
11
|
+
interface ConformanceTarget {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
createStorage(): Storage | Promise<Storage>;
|
|
14
|
+
cleanup?(keyPrefix: string): Promise<void>;
|
|
15
|
+
createStorageWithBadCredentials?(): Storage | Promise<Storage>;
|
|
16
|
+
createStorageWithExpiredCredentials?(): Storage | Promise<Storage>;
|
|
17
|
+
createStorageWithDeniedCredentials?(): Storage | Promise<Storage>;
|
|
18
|
+
}
|
|
19
|
+
interface ConformanceContext {
|
|
20
|
+
readonly storage: Storage;
|
|
21
|
+
readonly keyPrefix: string;
|
|
22
|
+
readonly target: ConformanceTarget;
|
|
23
|
+
declares(name: CapabilityName): boolean;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/case.d.ts
|
|
27
|
+
type ConformanceCase = {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly requires: readonly [];
|
|
30
|
+
readonly cost: "fast" | "slow";
|
|
31
|
+
run(ctx: ConformanceContext): Promise<void>;
|
|
32
|
+
} | {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly requires: readonly [CapabilityName, ...CapabilityName[]];
|
|
35
|
+
readonly cost: "fast" | "slow";
|
|
36
|
+
run(ctx: ConformanceContext): Promise<void>;
|
|
37
|
+
runWithout(ctx: ConformanceContext): Promise<void>;
|
|
38
|
+
};
|
|
39
|
+
interface ConformanceCaseMetadata {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly requires: readonly CapabilityName[];
|
|
42
|
+
readonly cost: "fast" | "slow";
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/cases/index.d.ts
|
|
46
|
+
/** The same cases as the package publishes them, without the factory of spec 9.3. */
|
|
47
|
+
export declare const conformanceCases: readonly ConformanceCase[];
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/result.d.ts
|
|
50
|
+
type ConformanceMode = "declared" | "without";
|
|
51
|
+
interface SerializedConformanceError {
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly message: string;
|
|
54
|
+
readonly stack?: string;
|
|
55
|
+
readonly code?: StorageErrorCode;
|
|
56
|
+
}
|
|
57
|
+
type ConformanceResult = {
|
|
58
|
+
readonly case: ConformanceCaseMetadata;
|
|
59
|
+
readonly status: "passed";
|
|
60
|
+
readonly mode: ConformanceMode;
|
|
61
|
+
} | {
|
|
62
|
+
readonly case: ConformanceCaseMetadata;
|
|
63
|
+
readonly status: "skipped";
|
|
64
|
+
readonly reason: string;
|
|
65
|
+
} | {
|
|
66
|
+
readonly case: ConformanceCaseMetadata;
|
|
67
|
+
readonly status: "failed";
|
|
68
|
+
readonly mode: ConformanceMode;
|
|
69
|
+
readonly error: SerializedConformanceError;
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/run.d.ts
|
|
73
|
+
interface ConformanceRunOptions {
|
|
74
|
+
includeSlow?: boolean;
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/describe.d.ts
|
|
78
|
+
interface ConformanceFramework extends ConformanceRunOptions {
|
|
79
|
+
describe(name: string, body: () => void): void;
|
|
80
|
+
test(name: string, body: () => Promise<void>): void;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Maps every case onto one test of Vitest, `bun:test` or `Deno.test`, whose `describe`
|
|
84
|
+
* and `test` agree in shape (ADR 0006), so that a failing case is a failing test of its
|
|
85
|
+
* own rather than one red line covering the suite.
|
|
86
|
+
*/
|
|
87
|
+
export declare function describeConformance(target: ConformanceTarget, framework: ConformanceFramework): void;
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/run-all.d.ts
|
|
90
|
+
/**
|
|
91
|
+
* Runs the cases without a test framework and reports them, which is how a runtime that
|
|
92
|
+
* has none — `workerd` — runs the suite (ADR 0006). A failing case is one result among
|
|
93
|
+
* the others; only a target that cannot produce a storage at all rejects the run.
|
|
94
|
+
*/
|
|
95
|
+
export declare function runAll(target: ConformanceTarget, options?: ConformanceRunOptions): Promise<readonly ConformanceResult[]>;
|
|
96
|
+
//#endregion
|
|
97
|
+
export type { ConformanceCase, ConformanceCaseMetadata, ConformanceContext, ConformanceFramework, ConformanceMode, ConformanceResult, ConformanceRunOptions, ConformanceTarget, SerializedConformanceError };
|