@schmock/faker 2.2.2 → 2.3.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/dist/field-mappings.d.ts.map +1 -1
- package/dist/field-mappings.js +8 -1
- package/dist/field-name-matcher.d.ts.map +1 -1
- package/dist/field-name-matcher.js +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/jsf-config.d.ts +11 -1
- package/dist/jsf-config.d.ts.map +1 -1
- package/dist/jsf-config.js +93 -11
- package/dist/overrides.d.ts.map +1 -1
- package/dist/overrides.js +20 -14
- package/dist/schema-enhancement.d.ts.map +1 -1
- package/dist/schema-enhancement.js +18 -3
- package/dist/test-utils.d.ts +9 -9
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/test-utils.js +25 -12
- package/dist/validation.js +1 -1
- package/package.json +4 -4
- package/src/field-mappings.ts +8 -1
- package/src/field-name-matcher.ts +4 -5
- package/src/index.test.ts +102 -0
- package/src/index.ts +59 -42
- package/src/jsf-config.test.ts +65 -0
- package/src/jsf-config.ts +129 -21
- package/src/overrides.ts +23 -20
- package/src/performance.test.ts +274 -327
- package/src/post-process.test.ts +87 -61
- package/src/schema-enhancement.test.ts +8 -2
- package/src/schema-enhancement.ts +24 -8
- package/src/steps/deterministic-seeds.steps.ts +77 -0
- package/src/steps/faker-plugin.steps.ts +198 -6
- package/src/test-utils.ts +48 -28
- package/src/validation.ts +1 -1
package/src/performance.test.ts
CHANGED
|
@@ -1,30 +1,61 @@
|
|
|
1
1
|
import type { JSONSchema7 } from "json-schema";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import { fakerPlugin, generateFromSchema } from "./index";
|
|
4
|
-
import { generate,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import { generate, schemas } from "./test-utils";
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function expectRecord(
|
|
11
|
+
value: unknown,
|
|
12
|
+
): asserts value is Record<string, unknown> {
|
|
13
|
+
expect(isRecord(value)).toBe(true);
|
|
14
|
+
if (!isRecord(value)) throw new Error("Expected generated object data");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function expectGeneratedUser(value: unknown): void {
|
|
18
|
+
expectRecord(value);
|
|
19
|
+
expect(value.id).toEqual(expect.any(String));
|
|
20
|
+
expect(value.email).toEqual(expect.any(String));
|
|
21
|
+
expect(value.firstName).toEqual(expect.any(String));
|
|
22
|
+
expect(value.lastName).toEqual(expect.any(String));
|
|
23
|
+
expect(value.createdAt).toEqual(expect.any(String));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pluginContext(): Schmock.PluginContext {
|
|
27
|
+
return {
|
|
28
|
+
method: "GET",
|
|
29
|
+
path: "/test",
|
|
30
|
+
params: {},
|
|
31
|
+
query: {},
|
|
32
|
+
state: new Map(),
|
|
33
|
+
routeState: {},
|
|
34
|
+
headers: {},
|
|
35
|
+
body: null,
|
|
36
|
+
route: {},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("Faker generation workload reliability", () => {
|
|
41
|
+
describe("Representative schema workloads", () => {
|
|
42
|
+
it("preserves simple object shape across repeated generation", async () => {
|
|
9
43
|
const schema = schemas.simple.object({
|
|
10
44
|
id: schemas.simple.number(),
|
|
11
45
|
name: schemas.simple.string(),
|
|
12
46
|
active: { type: "boolean" },
|
|
13
47
|
});
|
|
14
48
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
);
|
|
20
|
-
|
|
49
|
+
for (let seed = 0; seed < 10; seed += 1) {
|
|
50
|
+
const result = await generateFromSchema({ schema, seed });
|
|
51
|
+
expectRecord(result);
|
|
52
|
+
expect(result.id).toEqual(expect.any(Number));
|
|
53
|
+
expect(result.name).toEqual(expect.any(String));
|
|
54
|
+
expect(result.active).toEqual(expect.any(Boolean));
|
|
21
55
|
}
|
|
22
|
-
|
|
23
|
-
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
|
24
|
-
expect(avgTime).toBeLessThan(50); // Should average under 50ms
|
|
25
56
|
});
|
|
26
57
|
|
|
27
|
-
it("
|
|
58
|
+
it("preserves a three-level nested object shape", async () => {
|
|
28
59
|
const schema = schemas.nested.deep(
|
|
29
60
|
3,
|
|
30
61
|
schemas.simple.object({
|
|
@@ -33,14 +64,17 @@ describe("Performance and Memory", () => {
|
|
|
33
64
|
}),
|
|
34
65
|
);
|
|
35
66
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
67
|
+
let level = await generateFromSchema({ schema, seed: 42 });
|
|
68
|
+
for (let depth = 0; depth < 3; depth += 1) {
|
|
69
|
+
expectRecord(level);
|
|
70
|
+
level = level.nested;
|
|
71
|
+
}
|
|
72
|
+
expectRecord(level);
|
|
73
|
+
expect(level.id).toEqual(expect.any(Number));
|
|
74
|
+
expect(level.value).toEqual(expect.any(String));
|
|
41
75
|
});
|
|
42
76
|
|
|
43
|
-
it("generates
|
|
77
|
+
it("generates every item in a fixed-size object array", async () => {
|
|
44
78
|
const schema = schemas.simple.array(
|
|
45
79
|
schemas.simple.object({
|
|
46
80
|
id: schemas.simple.number(),
|
|
@@ -49,14 +83,18 @@ describe("Performance and Memory", () => {
|
|
|
49
83
|
{ minItems: 50, maxItems: 50 },
|
|
50
84
|
);
|
|
51
85
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
|
|
86
|
+
const result = await generateFromSchema({ schema, seed: 42 });
|
|
87
|
+
expect(Array.isArray(result)).toBe(true);
|
|
88
|
+
if (!Array.isArray(result)) throw new Error("Expected generated array");
|
|
89
|
+
expect(result).toHaveLength(50);
|
|
90
|
+
for (const item of result) {
|
|
91
|
+
expectRecord(item);
|
|
92
|
+
expect(item.id).toEqual(expect.any(Number));
|
|
93
|
+
expect(item.name).toEqual(expect.any(String));
|
|
94
|
+
}
|
|
57
95
|
});
|
|
58
96
|
|
|
59
|
-
it("
|
|
97
|
+
it("honors constraints in a complex collection schema", async () => {
|
|
60
98
|
const schema: JSONSchema7 = {
|
|
61
99
|
type: "object",
|
|
62
100
|
properties: {
|
|
@@ -80,389 +118,285 @@ describe("Performance and Memory", () => {
|
|
|
80
118
|
maxItems: 10,
|
|
81
119
|
},
|
|
82
120
|
},
|
|
121
|
+
required: ["users"],
|
|
83
122
|
};
|
|
84
123
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe("Scaling Behavior", () => {
|
|
94
|
-
it("scales linearly with array size", async () => {
|
|
95
|
-
const smallSchema = schemas.simple.array(schemas.simple.string(), {
|
|
96
|
-
minItems: 50,
|
|
97
|
-
maxItems: 50,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const largeSchema = schemas.simple.array(schemas.simple.string(), {
|
|
101
|
-
minItems: 500,
|
|
102
|
-
maxItems: 500,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Warmup runs to stabilize JIT
|
|
106
|
-
for (let i = 0; i < 3; i++) {
|
|
107
|
-
await generateFromSchema({ schema: smallSchema });
|
|
108
|
-
await generateFromSchema({ schema: largeSchema });
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Multiple measurement runs for statistical stability
|
|
112
|
-
const smallTimes: number[] = [];
|
|
113
|
-
const largeTimes: number[] = [];
|
|
114
|
-
|
|
115
|
-
for (let i = 0; i < 10; i++) {
|
|
116
|
-
const start1 = performance.now();
|
|
117
|
-
await generateFromSchema({ schema: smallSchema });
|
|
118
|
-
const small = performance.now() - start1;
|
|
119
|
-
smallTimes.push(small);
|
|
120
|
-
|
|
121
|
-
const start2 = performance.now();
|
|
122
|
-
await generateFromSchema({ schema: largeSchema });
|
|
123
|
-
const large = performance.now() - start2;
|
|
124
|
-
largeTimes.push(large);
|
|
124
|
+
const result = await generateFromSchema({ schema, seed: 42 });
|
|
125
|
+
expectRecord(result);
|
|
126
|
+
expect(Array.isArray(result.users)).toBe(true);
|
|
127
|
+
if (!Array.isArray(result.users)) {
|
|
128
|
+
throw new Error("Expected users collection");
|
|
125
129
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// Only check scaling if we have measurable timing
|
|
140
|
-
expect(medianLarge).toBeGreaterThan(medianSmall * 0.5); // Should take at least half as long
|
|
141
|
-
expect(medianLarge).toBeLessThan(medianSmall * 50); // But not extremely longer
|
|
130
|
+
expect(result.users).toHaveLength(10);
|
|
131
|
+
for (const user of result.users) {
|
|
132
|
+
expectRecord(user);
|
|
133
|
+
expect(user.id).toMatch(
|
|
134
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
135
|
+
);
|
|
136
|
+
expect(user.email).toEqual(expect.any(String));
|
|
137
|
+
expect(user.age).toBeGreaterThanOrEqual(18);
|
|
138
|
+
expect(user.age).toBeLessThanOrEqual(100);
|
|
139
|
+
expect(Array.isArray(user.tags)).toBe(true);
|
|
140
|
+
if (!Array.isArray(user.tags)) throw new Error("Expected user tags");
|
|
141
|
+
expect(user.tags.length).toBeLessThanOrEqual(5);
|
|
142
|
+
for (const tag of user.tags) expect(tag).toMatch(/^[a-z]+$/);
|
|
142
143
|
}
|
|
143
144
|
});
|
|
144
145
|
|
|
145
|
-
it("handles
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
146
|
+
it("handles both small and large fixed array workloads", async () => {
|
|
147
|
+
const sizes = [50, 500];
|
|
148
|
+
|
|
149
|
+
for (const size of sizes) {
|
|
150
|
+
const schema = schemas.simple.array(schemas.simple.string(), {
|
|
151
|
+
minItems: size,
|
|
152
|
+
maxItems: size,
|
|
153
|
+
});
|
|
154
|
+
const result = await generateFromSchema({ schema, seed: 42 });
|
|
155
|
+
expect(Array.isArray(result)).toBe(true);
|
|
156
|
+
if (!Array.isArray(result)) throw new Error("Expected generated array");
|
|
157
|
+
expect(result).toHaveLength(size);
|
|
158
|
+
expect(result.every((item) => typeof item === "string")).toBe(true);
|
|
153
159
|
}
|
|
160
|
+
});
|
|
154
161
|
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
wideTimes.push(performance.now() - start2);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Use median instead of average to reduce sensitivity to GC pauses
|
|
170
|
-
const median = (arr: number[]) => {
|
|
171
|
-
const sorted = [...arr].sort((a, b) => a - b);
|
|
172
|
-
const mid = Math.floor(sorted.length / 2);
|
|
173
|
-
return sorted.length % 2
|
|
174
|
-
? sorted[mid]
|
|
175
|
-
: (sorted[mid - 1] + sorted[mid]) / 2;
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const medNarrow = median(narrowTimes);
|
|
179
|
-
const medWide = median(wideTimes);
|
|
180
|
-
|
|
181
|
-
// Scale assertion (machine-speed-independent): wide has 5x the
|
|
182
|
-
// properties of narrow, so its generation time should scale roughly
|
|
183
|
-
// with that. Allow 20x headroom for measurement noise + small superlinear
|
|
184
|
-
// behavior; this catches order-of-magnitude regressions without flaking
|
|
185
|
-
// on shared CI runners where absolute wall-clock thresholds are unreliable.
|
|
186
|
-
if (medNarrow > 0.1) {
|
|
187
|
-
// Only check scaling if we have measurable timing
|
|
188
|
-
expect(medWide).toBeGreaterThan(medNarrow * 0.5); // sanity: more work, not less
|
|
189
|
-
expect(medWide).toBeLessThan(medNarrow * 20); // catches >4x-per-property regressions
|
|
162
|
+
it("handles both narrow and wide object workloads", async () => {
|
|
163
|
+
for (const width of [20, 100]) {
|
|
164
|
+
const result = await generateFromSchema({
|
|
165
|
+
schema: schemas.nested.wide(width),
|
|
166
|
+
seed: 42,
|
|
167
|
+
});
|
|
168
|
+
expectRecord(result);
|
|
169
|
+
expect(Object.keys(result)).toHaveLength(width);
|
|
170
|
+
expect(
|
|
171
|
+
Object.values(result).every((value) => typeof value === "string"),
|
|
172
|
+
).toBe(true);
|
|
190
173
|
}
|
|
191
174
|
});
|
|
192
175
|
});
|
|
193
176
|
|
|
194
|
-
describe("Plugin
|
|
195
|
-
it("plugin
|
|
196
|
-
const
|
|
177
|
+
describe("Plugin workloads", () => {
|
|
178
|
+
it("constructs a plugin with stable metadata", () => {
|
|
179
|
+
const plugin = fakerPlugin({ schema: schemas.complex.user() });
|
|
197
180
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
expect(duration).toBeLessThan(10); // Plugin creation should be instant
|
|
181
|
+
expect(plugin.name).toBe("faker");
|
|
182
|
+
expect(plugin.version).toMatch(/^\d+\.\d+\.\d+/);
|
|
201
183
|
});
|
|
202
184
|
|
|
203
|
-
it("
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
for (let i = 0; i < 10; i++) {
|
|
220
|
-
const { duration } = await perf.measure(() => plugin.process(context));
|
|
221
|
-
times.push(duration);
|
|
185
|
+
it("generates a complete response through repeated plugin processing", async () => {
|
|
186
|
+
const plugin = fakerPlugin({
|
|
187
|
+
schema: schemas.complex.apiResponse(),
|
|
188
|
+
seed: 42,
|
|
189
|
+
});
|
|
190
|
+
const context = pluginContext();
|
|
191
|
+
let firstResponse: unknown;
|
|
192
|
+
|
|
193
|
+
for (let iteration = 0; iteration < 10; iteration += 1) {
|
|
194
|
+
const result = await plugin.process(context);
|
|
195
|
+
expect(result.context).toBe(context);
|
|
196
|
+
expectRecord(result.response);
|
|
197
|
+
expect(result.response.success).toEqual(expect.any(Boolean));
|
|
198
|
+
expect(Array.isArray(result.response.data)).toBe(true);
|
|
199
|
+
if (iteration === 0) firstResponse = result.response;
|
|
200
|
+
else expect(result.response).toEqual(firstResponse);
|
|
222
201
|
}
|
|
223
|
-
|
|
224
|
-
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
|
225
|
-
expect(avgTime).toBeLessThan(100);
|
|
226
202
|
});
|
|
227
203
|
|
|
228
|
-
it("template
|
|
204
|
+
it("applies template overrides under a representative workload", async () => {
|
|
229
205
|
const schema = schemas.simple.object({
|
|
230
206
|
id: schemas.simple.string(),
|
|
231
207
|
userId: schemas.simple.string(),
|
|
232
208
|
timestamp: schemas.simple.string(),
|
|
233
209
|
message: schemas.simple.string(),
|
|
234
210
|
});
|
|
211
|
+
const timestamp = "2026-07-12T12:00:00.000Z";
|
|
212
|
+
|
|
213
|
+
const result = await generateFromSchema({
|
|
214
|
+
schema,
|
|
215
|
+
seed: 42,
|
|
216
|
+
overrides: {
|
|
217
|
+
id: "{{params.id}}",
|
|
218
|
+
userId: "{{state.user.id}}",
|
|
219
|
+
timestamp: "{{state.timestamp}}",
|
|
220
|
+
message: "User {{params.id}} at {{state.timestamp}}",
|
|
221
|
+
},
|
|
222
|
+
params: { id: "123" },
|
|
223
|
+
state: { user: { id: "user-456" }, timestamp },
|
|
224
|
+
});
|
|
235
225
|
|
|
236
|
-
|
|
237
|
-
id: "
|
|
238
|
-
userId: "
|
|
239
|
-
timestamp
|
|
240
|
-
message:
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const { duration } = await perf.measure(
|
|
244
|
-
async () =>
|
|
245
|
-
await generateFromSchema({
|
|
246
|
-
schema,
|
|
247
|
-
overrides,
|
|
248
|
-
params: { id: "123" },
|
|
249
|
-
state: {
|
|
250
|
-
user: { id: "user-456" },
|
|
251
|
-
timestamp: new Date().toISOString(),
|
|
252
|
-
},
|
|
253
|
-
}),
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
expect(duration).toBeLessThan(50); // Template processing should be fast
|
|
226
|
+
expect(result).toEqual({
|
|
227
|
+
id: "123",
|
|
228
|
+
userId: "user-456",
|
|
229
|
+
timestamp,
|
|
230
|
+
message: `User 123 at ${timestamp}`,
|
|
231
|
+
});
|
|
257
232
|
});
|
|
258
233
|
});
|
|
259
234
|
|
|
260
|
-
describe("Concurrent
|
|
261
|
-
it("
|
|
235
|
+
describe("Concurrent and repeated workloads", () => {
|
|
236
|
+
it("keeps twenty concurrent generations isolated and well formed", async () => {
|
|
262
237
|
const schema = schemas.complex.user();
|
|
238
|
+
const results = await Promise.all(
|
|
239
|
+
Array.from({ length: 20 }, (_, seed) =>
|
|
240
|
+
generateFromSchema({ schema, seed }),
|
|
241
|
+
),
|
|
242
|
+
);
|
|
263
243
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
await Promise.all(promises);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
expect(duration).toBeLessThan(500); // Should handle concurrency well
|
|
244
|
+
expect(results).toHaveLength(20);
|
|
245
|
+
for (const result of results) expectGeneratedUser(result);
|
|
246
|
+
expect(
|
|
247
|
+
new Set(results.map((result) => JSON.stringify(result))).size,
|
|
248
|
+
).toBe(20);
|
|
273
249
|
});
|
|
274
250
|
|
|
275
|
-
it("
|
|
251
|
+
it("preserves shape under a hundred sequential generations", async () => {
|
|
276
252
|
const schema = schemas.simple.object({
|
|
277
253
|
id: schemas.simple.number(),
|
|
278
254
|
data: schemas.simple.string(),
|
|
279
255
|
});
|
|
256
|
+
const results: unknown[] = [];
|
|
280
257
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const avgTime = duration / iterations;
|
|
293
|
-
expect(avgTime).toBeLessThan(10); // Should maintain speed
|
|
258
|
+
for (let seed = 0; seed < 100; seed += 1) {
|
|
259
|
+
const result = await generateFromSchema({ schema, seed });
|
|
260
|
+
expectRecord(result);
|
|
261
|
+
expect(result.id).toEqual(expect.any(Number));
|
|
262
|
+
expect(result.data).toEqual(expect.any(String));
|
|
263
|
+
results.push(result);
|
|
264
|
+
}
|
|
265
|
+
expect(
|
|
266
|
+
new Set(results.map((result) => JSON.stringify(result))).size,
|
|
267
|
+
).toBe(100);
|
|
294
268
|
});
|
|
295
|
-
});
|
|
296
269
|
|
|
297
|
-
|
|
298
|
-
it("doesn't leak memory on repeated generation", async () => {
|
|
270
|
+
it("preserves generated shape across a thousand calls", async () => {
|
|
299
271
|
const schema = schemas.simple.object({
|
|
300
272
|
id: schemas.simple.number(),
|
|
301
273
|
name: schemas.simple.string(),
|
|
302
274
|
});
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
for (let
|
|
306
|
-
const result = await generateFromSchema({ schema });
|
|
307
|
-
|
|
308
|
-
expect(result).
|
|
275
|
+
const serializedResults = new Set<string>();
|
|
276
|
+
|
|
277
|
+
for (let seed = 0; seed < 1_000; seed += 1) {
|
|
278
|
+
const result = await generateFromSchema({ schema, seed });
|
|
279
|
+
expectRecord(result);
|
|
280
|
+
expect(result.id).toEqual(expect.any(Number));
|
|
281
|
+
expect(result.name).toEqual(expect.any(String));
|
|
282
|
+
serializedResults.add(JSON.stringify(result));
|
|
309
283
|
}
|
|
310
284
|
|
|
311
|
-
|
|
312
|
-
expect(true).toBe(true);
|
|
285
|
+
expect(serializedResults.size).toBe(1_000);
|
|
313
286
|
});
|
|
314
287
|
|
|
315
|
-
it("
|
|
288
|
+
it("generates a thousand-item data structure", async () => {
|
|
316
289
|
const schema = schemas.simple.array(
|
|
317
290
|
schemas.simple.object({
|
|
318
291
|
id: schemas.simple.string(),
|
|
319
292
|
data: schemas.simple.string(),
|
|
320
293
|
}),
|
|
321
|
-
{ minItems:
|
|
294
|
+
{ minItems: 1_000, maxItems: 1_000 },
|
|
322
295
|
);
|
|
323
296
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
297
|
+
const result = await generateFromSchema({ schema, seed: 42 });
|
|
298
|
+
expect(Array.isArray(result)).toBe(true);
|
|
299
|
+
if (!Array.isArray(result)) throw new Error("Expected generated array");
|
|
300
|
+
expect(result).toHaveLength(1_000);
|
|
301
|
+
for (const item of result) {
|
|
302
|
+
expectRecord(item);
|
|
303
|
+
expect(item.id).toEqual(expect.any(String));
|
|
304
|
+
expect(item.data).toEqual(expect.any(String));
|
|
305
|
+
}
|
|
327
306
|
});
|
|
328
307
|
|
|
329
|
-
it("
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
308
|
+
it("recovers after repeated resource-limit failures", async () => {
|
|
309
|
+
const oversizedSchema = schemas.simple.array(schemas.simple.string(), {
|
|
310
|
+
minItems: 50_001,
|
|
311
|
+
maxItems: 50_001,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
for (let iteration = 0; iteration < 100; iteration += 1) {
|
|
315
|
+
await expect(
|
|
316
|
+
generateFromSchema({ schema: oversizedSchema }),
|
|
317
|
+
).rejects.toThrow("array_max_items");
|
|
337
318
|
}
|
|
338
319
|
|
|
339
|
-
|
|
340
|
-
|
|
320
|
+
const recovered = await generateFromSchema({
|
|
321
|
+
schema: schemas.simple.object({ id: schemas.simple.number() }),
|
|
322
|
+
seed: 42,
|
|
323
|
+
});
|
|
324
|
+
expectRecord(recovered);
|
|
325
|
+
expect(recovered.id).toEqual(expect.any(Number));
|
|
341
326
|
});
|
|
342
327
|
});
|
|
343
328
|
|
|
344
|
-
describe("
|
|
345
|
-
it("
|
|
329
|
+
describe("Repeated mapping and edge-case workloads", () => {
|
|
330
|
+
it("keeps smart field mapping valid across multiple seeds", async () => {
|
|
346
331
|
const schema = schemas.simple.object({
|
|
347
332
|
email: schemas.simple.string(),
|
|
348
333
|
firstName: schemas.simple.string(),
|
|
349
334
|
phone: schemas.simple.string(),
|
|
350
335
|
});
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
336
|
+
const results: Record<string, unknown>[] = [];
|
|
337
|
+
|
|
338
|
+
for (let seed = 0; seed < 10; seed += 1) {
|
|
339
|
+
const result = await generateFromSchema({ schema, seed });
|
|
340
|
+
expectRecord(result);
|
|
341
|
+
expect(result.email).toEqual(expect.any(String));
|
|
342
|
+
expect(result.firstName).toEqual(expect.any(String));
|
|
343
|
+
expect(result.phone).toEqual(expect.any(String));
|
|
356
344
|
results.push(result);
|
|
357
345
|
}
|
|
358
346
|
|
|
359
|
-
|
|
360
|
-
results.
|
|
361
|
-
expect(result).toHaveProperty("email");
|
|
362
|
-
expect(result).toHaveProperty("firstName");
|
|
363
|
-
expect(result).toHaveProperty("phone");
|
|
364
|
-
expect(typeof result.email).toBe("string");
|
|
365
|
-
expect(typeof result.firstName).toBe("string");
|
|
366
|
-
expect(typeof result.phone).toBe("string");
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
// Verify that smart field mapping worked across all calls
|
|
370
|
-
const emails = results.map((r) => r.email);
|
|
371
|
-
const firstNames = results.map((r) => r.firstName);
|
|
372
|
-
|
|
373
|
-
// Should have good diversity (no exact duplicates expected)
|
|
374
|
-
const uniqueEmails = new Set(emails);
|
|
375
|
-
const uniqueFirstNames = new Set(firstNames);
|
|
376
|
-
expect(uniqueEmails.size).toBeGreaterThan(1);
|
|
377
|
-
expect(uniqueFirstNames.size).toBeGreaterThan(1);
|
|
347
|
+
expect(new Set(results.map((result) => result.email)).size).toBe(10);
|
|
348
|
+
expect(new Set(results.map((result) => result.firstName)).size).toBe(10);
|
|
378
349
|
});
|
|
379
350
|
|
|
380
|
-
it("reuses
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
const context = {
|
|
385
|
-
method: "GET",
|
|
386
|
-
path: "/test",
|
|
387
|
-
params: {},
|
|
388
|
-
query: {},
|
|
389
|
-
state: {},
|
|
390
|
-
headers: {},
|
|
391
|
-
body: null,
|
|
392
|
-
route: {},
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
// Warmup
|
|
396
|
-
for (let i = 0; i < 3; i++) {
|
|
397
|
-
plugin.process(context);
|
|
398
|
-
}
|
|
351
|
+
it("reuses one plugin without changing seeded output", async () => {
|
|
352
|
+
const plugin = fakerPlugin({ schema: schemas.complex.user(), seed: 42 });
|
|
353
|
+
const context = pluginContext();
|
|
354
|
+
const responses: unknown[] = [];
|
|
399
355
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
plugin.process(context);
|
|
405
|
-
times.push(performance.now() - start);
|
|
356
|
+
for (let iteration = 0; iteration < 15; iteration += 1) {
|
|
357
|
+
const result = await plugin.process(context);
|
|
358
|
+
expectGeneratedUser(result.response);
|
|
359
|
+
responses.push(result.response);
|
|
406
360
|
}
|
|
407
361
|
|
|
408
|
-
|
|
409
|
-
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
|
410
|
-
const maxTime = Math.max(...times);
|
|
411
|
-
const minTime = Math.min(...times);
|
|
412
|
-
|
|
413
|
-
// Performance should be consistent and reasonable
|
|
414
|
-
expect(avgTime).toBeLessThan(50); // Should be reasonably fast
|
|
415
|
-
expect(maxTime).toBeLessThan(100); // No extreme outliers
|
|
416
|
-
expect(minTime).toBeGreaterThanOrEqual(0); // Valid timing
|
|
417
|
-
|
|
418
|
-
// All measurements should be reasonable - no extreme variance
|
|
419
|
-
const reasonableMaxTime = Math.max(avgTime * 5, 10); // At least 10ms tolerance
|
|
420
|
-
expect(maxTime).toBeLessThan(reasonableMaxTime);
|
|
362
|
+
expect(responses).toEqual(Array.from({ length: 15 }, () => responses[0]));
|
|
421
363
|
});
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
describe("Edge Case Performance", () => {
|
|
425
|
-
it("handles empty schemas efficiently", async () => {
|
|
426
|
-
const schema = schemas.simple.object({});
|
|
427
|
-
|
|
428
|
-
const { duration } = await perf.measure(
|
|
429
|
-
async () => await generateFromSchema({ schema }),
|
|
430
|
-
);
|
|
431
364
|
|
|
432
|
-
|
|
365
|
+
it("handles an empty object schema", async () => {
|
|
366
|
+
await expect(
|
|
367
|
+
generateFromSchema({ schema: schemas.simple.object({}), seed: 42 }),
|
|
368
|
+
).resolves.toEqual({});
|
|
433
369
|
});
|
|
434
370
|
|
|
435
|
-
it("
|
|
371
|
+
it("selects only declared values from a large enum", async () => {
|
|
372
|
+
const countries = Array.from(
|
|
373
|
+
{ length: 200 },
|
|
374
|
+
(_, index) => `country-${index}`,
|
|
375
|
+
);
|
|
436
376
|
const schema = schemas.simple.object({
|
|
437
|
-
country: {
|
|
438
|
-
type: "string",
|
|
439
|
-
enum: Array.from({ length: 200 }, (_, i) => `country-${i}`),
|
|
440
|
-
},
|
|
377
|
+
country: { type: "string", enum: countries },
|
|
441
378
|
});
|
|
379
|
+
const result = await generateFromSchema({ schema, seed: 42 });
|
|
442
380
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
);
|
|
446
|
-
|
|
447
|
-
expect(duration).toBeLessThan(50); // Large enum shouldn't be slow
|
|
381
|
+
expectRecord(result);
|
|
382
|
+
expect(countries).toContain(result.country);
|
|
448
383
|
});
|
|
449
384
|
|
|
450
|
-
it("
|
|
385
|
+
it("generates samples matching a complex pattern", async () => {
|
|
386
|
+
const pattern = /^[A-Z]{2}-[0-9]{4}-[a-z]{2}-[0-9A-F]{8}$/;
|
|
451
387
|
const schema = schemas.simple.object({
|
|
452
|
-
code: {
|
|
453
|
-
type: "string",
|
|
454
|
-
pattern: "^[A-Z]{2}-[0-9]{4}-[a-z]{2}-[0-9A-F]{8}$",
|
|
455
|
-
},
|
|
388
|
+
code: { type: "string", pattern: pattern.source },
|
|
456
389
|
});
|
|
390
|
+
const results = await generate.samples(schema, 10, { seed: 42 });
|
|
457
391
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
392
|
+
expect(results).toHaveLength(10);
|
|
393
|
+
for (const result of results) {
|
|
394
|
+
expectRecord(result);
|
|
395
|
+
expect(result.code).toMatch(pattern);
|
|
396
|
+
}
|
|
463
397
|
});
|
|
464
398
|
|
|
465
|
-
it("
|
|
399
|
+
it("satisfies one branch of a mixed constraint schema", async () => {
|
|
466
400
|
const schema: JSONSchema7 = {
|
|
467
401
|
anyOf: [
|
|
468
402
|
{
|
|
@@ -471,10 +405,11 @@ describe("Performance and Memory", () => {
|
|
|
471
405
|
type: { const: "A" },
|
|
472
406
|
data: { type: "string", minLength: 100 },
|
|
473
407
|
},
|
|
408
|
+
required: ["type", "data"],
|
|
474
409
|
},
|
|
475
410
|
{
|
|
476
411
|
type: "array",
|
|
477
|
-
items: { type: "number", minimum: 0, maximum:
|
|
412
|
+
items: { type: "number", minimum: 0, maximum: 1_000 },
|
|
478
413
|
minItems: 50,
|
|
479
414
|
},
|
|
480
415
|
{
|
|
@@ -484,11 +419,23 @@ describe("Performance and Memory", () => {
|
|
|
484
419
|
],
|
|
485
420
|
};
|
|
486
421
|
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
422
|
+
const result = await generateFromSchema({ schema, seed: 42 });
|
|
423
|
+
const isValidObject =
|
|
424
|
+
isRecord(result) &&
|
|
425
|
+
result.type === "A" &&
|
|
426
|
+
typeof result.data === "string" &&
|
|
427
|
+
result.data.length >= 100;
|
|
428
|
+
const isValidArray =
|
|
429
|
+
Array.isArray(result) &&
|
|
430
|
+
result.length >= 50 &&
|
|
431
|
+
result.every(
|
|
432
|
+
(value) => typeof value === "number" && value >= 0 && value <= 1_000,
|
|
433
|
+
);
|
|
434
|
+
const isValidString =
|
|
435
|
+
typeof result === "string" &&
|
|
436
|
+
/^[A-Za-z0-9+/]{100,}={0,2}$/.test(result);
|
|
490
437
|
|
|
491
|
-
expect(
|
|
438
|
+
expect(isValidObject || isValidArray || isValidString).toBe(true);
|
|
492
439
|
});
|
|
493
440
|
});
|
|
494
441
|
});
|