@schmock/schema 1.0.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.
@@ -0,0 +1,477 @@
1
+ import type { JSONSchema7 } from "json-schema";
2
+ import { describe, expect, it } from "vitest";
3
+ import { generateFromSchema, schemaPlugin } from "./index";
4
+ import { generate, performance as perf, schemas } from "./test-utils";
5
+
6
+ describe("Performance and Memory", () => {
7
+ describe("Generation Speed", () => {
8
+ it("generates simple objects quickly", async () => {
9
+ const schema = schemas.simple.object({
10
+ id: schemas.simple.number(),
11
+ name: schemas.simple.string(),
12
+ active: { type: "boolean" },
13
+ });
14
+
15
+ const times: number[] = [];
16
+ for (let i = 0; i < 10; i++) {
17
+ const { duration } = await perf.measure(() =>
18
+ generateFromSchema({ schema }),
19
+ );
20
+ times.push(duration);
21
+ }
22
+
23
+ const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
24
+ expect(avgTime).toBeLessThan(50); // Should average under 50ms
25
+ });
26
+
27
+ it("handles nested objects efficiently", async () => {
28
+ const schema = schemas.nested.deep(
29
+ 3,
30
+ schemas.simple.object({
31
+ id: schemas.simple.number(),
32
+ value: schemas.simple.string(),
33
+ }),
34
+ );
35
+
36
+ const { duration } = await perf.measure(() =>
37
+ generateFromSchema({ schema }),
38
+ );
39
+
40
+ expect(duration).toBeLessThan(100); // Reasonable for nested structure
41
+ });
42
+
43
+ it("generates arrays efficiently", async () => {
44
+ const schema = schemas.simple.array(
45
+ schemas.simple.object({
46
+ id: schemas.simple.number(),
47
+ name: schemas.simple.string(),
48
+ }),
49
+ { minItems: 50, maxItems: 50 },
50
+ );
51
+
52
+ const { duration } = await perf.measure(() =>
53
+ generateFromSchema({ schema }),
54
+ );
55
+
56
+ expect(duration).toBeLessThan(200); // Should handle 50 items quickly
57
+ });
58
+
59
+ it("handles complex schemas with multiple constraints", async () => {
60
+ const schema: JSONSchema7 = {
61
+ type: "object",
62
+ properties: {
63
+ users: {
64
+ type: "array",
65
+ items: {
66
+ type: "object",
67
+ properties: {
68
+ id: { type: "string", format: "uuid" },
69
+ email: { type: "string", format: "email" },
70
+ age: { type: "integer", minimum: 18, maximum: 100 },
71
+ tags: {
72
+ type: "array",
73
+ items: { type: "string", pattern: "^[a-z]+$" },
74
+ maxItems: 5,
75
+ },
76
+ },
77
+ required: ["id", "email"],
78
+ },
79
+ minItems: 10,
80
+ maxItems: 10,
81
+ },
82
+ },
83
+ };
84
+
85
+ const { duration } = await perf.measure(() =>
86
+ generateFromSchema({ schema }),
87
+ );
88
+
89
+ expect(duration).toBeLessThan(300); // Complex but still reasonable
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
+ generateFromSchema({ schema: smallSchema });
108
+ 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
+ generateFromSchema({ schema: smallSchema });
118
+ const small = performance.now() - start1;
119
+ smallTimes.push(small);
120
+
121
+ const start2 = performance.now();
122
+ generateFromSchema({ schema: largeSchema });
123
+ const large = performance.now() - start2;
124
+ largeTimes.push(large);
125
+ }
126
+
127
+ // Remove outliers and calculate medians for stability
128
+ smallTimes.sort((a, b) => a - b);
129
+ largeTimes.sort((a, b) => a - b);
130
+ const medianSmall = smallTimes[Math.floor(smallTimes.length / 2)];
131
+ const medianLarge = largeTimes[Math.floor(largeTimes.length / 2)];
132
+
133
+ // Both should complete in reasonable time (main goal is to ensure it works, not strict timing)
134
+ expect(medianSmall).toBeLessThan(50); // Small arrays should be fast
135
+ expect(medianLarge).toBeLessThan(200); // Large arrays should still be reasonable
136
+
137
+ // Optional: Check scaling if timing is meaningful
138
+ if (medianSmall > 0.1) {
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
142
+ }
143
+ });
144
+
145
+ it("handles wide objects efficiently", async () => {
146
+ const narrowSchema = schemas.nested.wide(20);
147
+ const wideSchema = schemas.nested.wide(100);
148
+
149
+ // Warmup
150
+ for (let i = 0; i < 3; i++) {
151
+ generateFromSchema({ schema: narrowSchema });
152
+ generateFromSchema({ schema: wideSchema });
153
+ }
154
+
155
+ // Measure with multiple runs
156
+ const narrowTimes: number[] = [];
157
+ const wideTimes: number[] = [];
158
+
159
+ for (let i = 0; i < 8; i++) {
160
+ const start1 = performance.now();
161
+ generateFromSchema({ schema: narrowSchema });
162
+ narrowTimes.push(performance.now() - start1);
163
+
164
+ const start2 = performance.now();
165
+ generateFromSchema({ schema: wideSchema });
166
+ wideTimes.push(performance.now() - start2);
167
+ }
168
+
169
+ const avgNarrow =
170
+ narrowTimes.reduce((a, b) => a + b, 0) / narrowTimes.length;
171
+ const avgWide = wideTimes.reduce((a, b) => a + b, 0) / wideTimes.length;
172
+
173
+ // 5x more properties should take more time but scale reasonably
174
+ expect(avgWide).toBeGreaterThan(avgNarrow);
175
+ expect(avgWide).toBeLessThan(avgNarrow * 15); // Linear-ish scaling, not exponential
176
+ });
177
+ });
178
+
179
+ describe("Plugin Performance", () => {
180
+ it("plugin creation is fast", async () => {
181
+ const schema = schemas.complex.user();
182
+
183
+ const { duration } = await perf.measure(() => schemaPlugin({ schema }));
184
+
185
+ expect(duration).toBeLessThan(10); // Plugin creation should be instant
186
+ });
187
+
188
+ it("plugin processing adds minimal overhead", async () => {
189
+ const schema = schemas.complex.apiResponse();
190
+ const plugin = schemaPlugin({ schema });
191
+
192
+ const context = {
193
+ method: "GET",
194
+ path: "/test",
195
+ params: {},
196
+ query: {},
197
+ state: {},
198
+ headers: {},
199
+ body: null,
200
+ route: {},
201
+ };
202
+
203
+ const times: number[] = [];
204
+ for (let i = 0; i < 10; i++) {
205
+ const { duration } = await perf.measure(() => plugin.process(context));
206
+ times.push(duration);
207
+ }
208
+
209
+ const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
210
+ expect(avgTime).toBeLessThan(100);
211
+ });
212
+
213
+ it("template processing is efficient", async () => {
214
+ const schema = schemas.simple.object({
215
+ id: schemas.simple.string(),
216
+ userId: schemas.simple.string(),
217
+ timestamp: schemas.simple.string(),
218
+ message: schemas.simple.string(),
219
+ });
220
+
221
+ const overrides = {
222
+ id: "{{params.id}}",
223
+ userId: "{{state.user.id}}",
224
+ timestamp: "{{state.timestamp}}",
225
+ message: "User {{params.id}} at {{state.timestamp}}",
226
+ };
227
+
228
+ const { duration } = await perf.measure(() =>
229
+ generateFromSchema({
230
+ schema,
231
+ overrides,
232
+ params: { id: "123" },
233
+ state: {
234
+ user: { id: "user-456" },
235
+ timestamp: new Date().toISOString(),
236
+ },
237
+ }),
238
+ );
239
+
240
+ expect(duration).toBeLessThan(50); // Template processing should be fast
241
+ });
242
+ });
243
+
244
+ describe("Concurrent Generation", () => {
245
+ it("handles multiple concurrent generations", async () => {
246
+ const schema = schemas.complex.user();
247
+
248
+ const { duration } = await perf.measure(async () => {
249
+ const promises = Array.from({ length: 20 }, () =>
250
+ generateFromSchema({ schema }),
251
+ );
252
+ await Promise.all(promises);
253
+ });
254
+
255
+ expect(duration).toBeLessThan(500); // Should handle concurrency well
256
+ });
257
+
258
+ it("maintains performance under load", async () => {
259
+ const schema = schemas.simple.object({
260
+ id: schemas.simple.number(),
261
+ data: schemas.simple.string(),
262
+ });
263
+
264
+ // Warm up
265
+ generateFromSchema({ schema });
266
+
267
+ // Test under load
268
+ const iterations = 100;
269
+ const { duration } = await perf.measure(async () => {
270
+ for (let i = 0; i < iterations; i++) {
271
+ generateFromSchema({ schema });
272
+ }
273
+ });
274
+
275
+ const avgTime = duration / iterations;
276
+ expect(avgTime).toBeLessThan(10); // Should maintain speed
277
+ });
278
+ });
279
+
280
+ describe("Memory Efficiency", () => {
281
+ it("doesn't leak memory on repeated generation", () => {
282
+ const schema = schemas.simple.object({
283
+ id: schemas.simple.number(),
284
+ name: schemas.simple.string(),
285
+ });
286
+
287
+ // Generate many times
288
+ for (let i = 0; i < 1000; i++) {
289
+ const result = generateFromSchema({ schema });
290
+ // Result should be garbage collectable
291
+ expect(result).toBeDefined();
292
+ }
293
+
294
+ // If we got here without crashing, memory is managed well
295
+ expect(true).toBe(true);
296
+ });
297
+
298
+ it("handles large data structures without excessive memory", () => {
299
+ const schema = schemas.simple.array(
300
+ schemas.simple.object({
301
+ id: schemas.simple.string(),
302
+ data: schemas.simple.string(),
303
+ }),
304
+ { minItems: 1000, maxItems: 1000 },
305
+ );
306
+
307
+ // Should be able to generate without memory issues
308
+ const result = generateFromSchema({ schema });
309
+ expect(result).toHaveLength(1000);
310
+ });
311
+
312
+ it("cleans up after schema validation errors", () => {
313
+ // Generate errors repeatedly
314
+ for (let i = 0; i < 100; i++) {
315
+ try {
316
+ generateFromSchema({ schema: { type: "invalid" as any } });
317
+ } catch (_e) {
318
+ // Expected
319
+ }
320
+ }
321
+
322
+ // Should not have memory leaks from error objects
323
+ expect(true).toBe(true);
324
+ });
325
+ });
326
+
327
+ describe("Optimization Opportunities", () => {
328
+ it("generates data consistently across multiple calls", async () => {
329
+ const schema = schemas.simple.object({
330
+ email: schemas.simple.string(),
331
+ firstName: schemas.simple.string(),
332
+ phone: schemas.simple.string(),
333
+ });
334
+
335
+ // Generate multiple times to ensure consistent behavior
336
+ const results: any[] = [];
337
+ for (let i = 0; i < 10; i++) {
338
+ const result = generateFromSchema({ schema });
339
+ results.push(result);
340
+ }
341
+
342
+ // Verify all results have the expected structure
343
+ results.forEach((result) => {
344
+ expect(result).toHaveProperty("email");
345
+ expect(result).toHaveProperty("firstName");
346
+ expect(result).toHaveProperty("phone");
347
+ expect(typeof result.email).toBe("string");
348
+ expect(typeof result.firstName).toBe("string");
349
+ expect(typeof result.phone).toBe("string");
350
+ });
351
+
352
+ // Verify that smart field mapping worked across all calls
353
+ const emails = results.map((r) => r.email);
354
+ const firstNames = results.map((r) => r.firstName);
355
+
356
+ // Should have good diversity (no exact duplicates expected)
357
+ const uniqueEmails = new Set(emails);
358
+ const uniqueFirstNames = new Set(firstNames);
359
+ expect(uniqueEmails.size).toBeGreaterThan(1);
360
+ expect(uniqueFirstNames.size).toBeGreaterThan(1);
361
+ });
362
+
363
+ it("reuses schema enhancement for repeated generations", async () => {
364
+ const schema = schemas.complex.user();
365
+ const plugin = schemaPlugin({ schema });
366
+
367
+ const context = {
368
+ method: "GET",
369
+ path: "/test",
370
+ params: {},
371
+ query: {},
372
+ state: {},
373
+ headers: {},
374
+ body: null,
375
+ route: {},
376
+ };
377
+
378
+ // Warmup
379
+ for (let i = 0; i < 3; i++) {
380
+ plugin.process(context);
381
+ }
382
+
383
+ // Multiple calls through same plugin with proper timing
384
+ const times: number[] = [];
385
+ for (let i = 0; i < 15; i++) {
386
+ const start = performance.now();
387
+ plugin.process(context);
388
+ times.push(performance.now() - start);
389
+ }
390
+
391
+ // Calculate statistics
392
+ const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
393
+ const maxTime = Math.max(...times);
394
+ const minTime = Math.min(...times);
395
+
396
+ // Performance should be consistent and reasonable
397
+ expect(avgTime).toBeLessThan(50); // Should be reasonably fast
398
+ expect(maxTime).toBeLessThan(100); // No extreme outliers
399
+ expect(minTime).toBeGreaterThanOrEqual(0); // Valid timing
400
+
401
+ // All measurements should be reasonable - no extreme variance
402
+ const reasonableMaxTime = Math.max(avgTime * 5, 10); // At least 10ms tolerance
403
+ expect(maxTime).toBeLessThan(reasonableMaxTime);
404
+ });
405
+ });
406
+
407
+ describe("Edge Case Performance", () => {
408
+ it("handles empty schemas efficiently", async () => {
409
+ const schema = schemas.simple.object({});
410
+
411
+ const { duration } = await perf.measure(() =>
412
+ generateFromSchema({ schema }),
413
+ );
414
+
415
+ expect(duration).toBeLessThan(10); // Empty should be instant
416
+ });
417
+
418
+ it("handles schemas with many enum values", async () => {
419
+ const schema = schemas.simple.object({
420
+ country: {
421
+ type: "string",
422
+ enum: Array.from({ length: 200 }, (_, i) => `country-${i}`),
423
+ },
424
+ });
425
+
426
+ const { duration } = await perf.measure(() =>
427
+ generateFromSchema({ schema }),
428
+ );
429
+
430
+ expect(duration).toBeLessThan(50); // Large enum shouldn't be slow
431
+ });
432
+
433
+ it("handles complex regex patterns efficiently", async () => {
434
+ const schema = schemas.simple.object({
435
+ code: {
436
+ type: "string",
437
+ pattern: "^[A-Z]{2}-[0-9]{4}-[a-z]{2}-[0-9A-F]{8}$",
438
+ },
439
+ });
440
+
441
+ const { duration } = await perf.measure(() =>
442
+ generate.samples(schema, 10),
443
+ );
444
+
445
+ expect(duration).toBeLessThan(100); // Complex patterns OK
446
+ });
447
+
448
+ it("handles mixed constraint schemas", async () => {
449
+ const schema: JSONSchema7 = {
450
+ anyOf: [
451
+ {
452
+ type: "object",
453
+ properties: {
454
+ type: { const: "A" },
455
+ data: { type: "string", minLength: 100 },
456
+ },
457
+ },
458
+ {
459
+ type: "array",
460
+ items: { type: "number", minimum: 0, maximum: 1000 },
461
+ minItems: 50,
462
+ },
463
+ {
464
+ type: "string",
465
+ pattern: "^[A-Za-z0-9+/]{100,}={0,2}$",
466
+ },
467
+ ],
468
+ };
469
+
470
+ const { duration } = await perf.measure(() =>
471
+ generateFromSchema({ schema }),
472
+ );
473
+
474
+ expect(duration).toBeLessThan(200); // Complex but manageable
475
+ });
476
+ });
477
+ });