Skip to content

Commit e9b943a

Browse files
authored
Merge pull request #12 from jsonjoy-com/copilot/fix-11
[WIP] Refactor type `random()` methods
2 parents b93b84b + fe49821 commit e9b943a

20 files changed

+585
-134
lines changed

src/__tests__/fixtures.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Fixture schemas for testing random value generation.
3+
* These schemas represent different JSON Type configurations that can be used
4+
* across multiple test modules.
5+
*/
6+
7+
import {s} from '../schema';
8+
9+
/**
10+
* Basic primitive type schemas
11+
*/
12+
export const primitiveSchemas = {
13+
string: s.String(),
14+
stringWithMinMax: s.String({min: 5, max: 10}),
15+
number: s.Number(),
16+
numberWithFormat: s.Number({format: 'u32'}),
17+
numberWithRange: s.Number({gte: 0, lte: 100}),
18+
boolean: s.Boolean(),
19+
const: s.Const('fixed-value' as const),
20+
any: s.Any(),
21+
} as const;
22+
23+
/**
24+
* Complex composite type schemas
25+
*/
26+
export const compositeSchemas = {
27+
simpleArray: s.Array(s.String()),
28+
arrayWithBounds: s.Array(s.Number(), {min: 2, max: 5}),
29+
simpleObject: s.Object([s.prop('id', s.String()), s.prop('name', s.String()), s.prop('active', s.Boolean())]),
30+
objectWithOptionalFields: s.Object([
31+
s.prop('id', s.String()),
32+
s.propOpt('name', s.String()),
33+
s.propOpt('count', s.Number()),
34+
]),
35+
nestedObject: s.Object([
36+
s.prop(
37+
'user',
38+
s.Object([
39+
s.prop('id', s.Number()),
40+
s.prop('profile', s.Object([s.prop('name', s.String()), s.prop('email', s.String())])),
41+
]),
42+
),
43+
s.prop('tags', s.Array(s.String())),
44+
]),
45+
tuple: s.Tuple(s.String(), s.Number(), s.Boolean()),
46+
map: s.Map(s.String()),
47+
mapWithComplexValue: s.Map(s.Object([s.prop('value', s.Number()), s.prop('label', s.String())])),
48+
union: s.Or(s.String(), s.Number(), s.Boolean()),
49+
complexUnion: s.Or(
50+
s.String(),
51+
s.Object([s.prop('type', s.Const('object' as const)), s.prop('data', s.Any())]),
52+
s.Array(s.Number()),
53+
),
54+
binary: s.bin,
55+
} as const;
56+
57+
/**
58+
* All fixture schemas combined for comprehensive testing
59+
*/
60+
export const allSchemas = {
61+
...primitiveSchemas,
62+
...compositeSchemas,
63+
} as const;
64+
65+
/**
66+
* Schema categories for organized testing
67+
*/
68+
export const schemaCategories = {
69+
primitives: primitiveSchemas,
70+
composites: compositeSchemas,
71+
all: allSchemas,
72+
} as const;
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/**
2+
* Unit tests for the src/random/ module.
3+
* Tests that generated random values conform to their JSON Type schemas.
4+
*/
5+
6+
import {t} from '../../type';
7+
import {allSchemas, schemaCategories} from '../../__tests__/fixtures';
8+
import * as gen from '../generators';
9+
import {random} from '../generator';
10+
11+
describe('random generators', () => {
12+
describe('individual generator functions', () => {
13+
describe('primitives', () => {
14+
test('str generates valid strings', () => {
15+
const type = t.String();
16+
for (let i = 0; i < 10; i++) {
17+
const value = gen.str(type);
18+
expect(typeof value).toBe('string');
19+
type.validate(value);
20+
}
21+
});
22+
23+
test('str respects min/max constraints', () => {
24+
const type = t.String({min: 5, max: 10});
25+
for (let i = 0; i < 10; i++) {
26+
const value = gen.str(type);
27+
expect(typeof value).toBe('string');
28+
expect(value.length).toBeGreaterThanOrEqual(5);
29+
expect(value.length).toBeLessThanOrEqual(10);
30+
type.validate(value);
31+
}
32+
});
33+
34+
test('num generates valid numbers', () => {
35+
const type = t.Number();
36+
for (let i = 0; i < 10; i++) {
37+
const value = gen.num(type);
38+
expect(typeof value).toBe('number');
39+
type.validate(value);
40+
}
41+
});
42+
43+
test('num respects format constraints', () => {
44+
const type = t.Number({format: 'u32'});
45+
for (let i = 0; i < 10; i++) {
46+
const value = gen.num(type);
47+
expect(typeof value).toBe('number');
48+
expect(Number.isInteger(value)).toBe(true);
49+
expect(value).toBeGreaterThanOrEqual(0);
50+
expect(value).toBeLessThanOrEqual(0xffffffff);
51+
type.validate(value);
52+
}
53+
});
54+
55+
test('bool generates valid booleans', () => {
56+
const type = t.Boolean();
57+
for (let i = 0; i < 10; i++) {
58+
const value = gen.bool(type);
59+
expect(typeof value).toBe('boolean');
60+
type.validate(value);
61+
}
62+
});
63+
64+
test('const_ generates exact values', () => {
65+
const type = t.Const('fixed-value' as const);
66+
for (let i = 0; i < 10; i++) {
67+
const value = gen.const_(type);
68+
expect(value).toBe('fixed-value');
69+
type.validate(value);
70+
}
71+
});
72+
73+
test('any generates valid JSON values', () => {
74+
const type = t.Any();
75+
for (let i = 0; i < 10; i++) {
76+
const value = gen.any(type);
77+
expect(value).toBeDefined();
78+
type.validate(value);
79+
}
80+
});
81+
82+
test('bin generates Uint8Array', () => {
83+
const type = t.bin;
84+
for (let i = 0; i < 10; i++) {
85+
const value = gen.bin(type);
86+
expect(value).toBeInstanceOf(Uint8Array);
87+
type.validate(value);
88+
}
89+
});
90+
});
91+
92+
describe('composites', () => {
93+
test('arr generates valid arrays', () => {
94+
const type = t.Array(t.String());
95+
for (let i = 0; i < 10; i++) {
96+
const value = gen.arr(type);
97+
expect(Array.isArray(value)).toBe(true);
98+
type.validate(value);
99+
}
100+
});
101+
102+
test('arr respects min/max constraints', () => {
103+
const type = t.Array(t.String(), {min: 2, max: 5});
104+
for (let i = 0; i < 10; i++) {
105+
const value = gen.arr(type);
106+
expect(Array.isArray(value)).toBe(true);
107+
expect(value.length).toBeGreaterThanOrEqual(2);
108+
expect(value.length).toBeLessThanOrEqual(5);
109+
type.validate(value);
110+
}
111+
});
112+
113+
test('obj generates valid objects', () => {
114+
const type = t.Object(t.prop('id', t.String()), t.prop('count', t.Number()));
115+
for (let i = 0; i < 10; i++) {
116+
const value = gen.obj(type);
117+
expect(typeof value).toBe('object');
118+
expect(value).not.toBeNull();
119+
expect(value).not.toBeInstanceOf(Array);
120+
expect(value).toHaveProperty('id');
121+
expect(value).toHaveProperty('count');
122+
type.validate(value);
123+
}
124+
});
125+
126+
test('tup generates valid tuples', () => {
127+
const type = t.Tuple(t.String(), t.Number(), t.Boolean());
128+
for (let i = 0; i < 10; i++) {
129+
const value = gen.tup(type);
130+
expect(Array.isArray(value)).toBe(true);
131+
expect(value).toHaveLength(3);
132+
expect(typeof value[0]).toBe('string');
133+
expect(typeof value[1]).toBe('number');
134+
expect(typeof value[2]).toBe('boolean');
135+
type.validate(value);
136+
}
137+
});
138+
139+
test('map generates valid maps', () => {
140+
const type = t.Map(t.String());
141+
for (let i = 0; i < 10; i++) {
142+
const value = gen.map(type);
143+
expect(typeof value).toBe('object');
144+
expect(value).not.toBeNull();
145+
expect(value).not.toBeInstanceOf(Array);
146+
type.validate(value);
147+
}
148+
});
149+
150+
test('or generates values from union types', () => {
151+
const type = t.Or(t.String(), t.Number());
152+
const generatedTypes = new Set<string>();
153+
154+
for (let i = 0; i < 20; i++) {
155+
const value = gen.or(type);
156+
generatedTypes.add(typeof value);
157+
type.validate(value);
158+
}
159+
160+
// Should generate at least one of each type over multiple iterations
161+
expect(generatedTypes.size).toBeGreaterThan(0);
162+
});
163+
164+
test('fn generates async functions', async () => {
165+
const type = t.Function(t.num, t.String());
166+
const value = gen.fn(type);
167+
expect(typeof value).toBe('function');
168+
169+
// Test that the function is async and returns the expected type
170+
const result = await (value as () => Promise<unknown>)();
171+
expect(typeof result).toBe('string');
172+
});
173+
});
174+
});
175+
176+
describe('main router function', () => {
177+
test('dispatches to correct generators for all types', () => {
178+
for (const [name, schema] of Object.entries(schemaCategories.primitives)) {
179+
const type = t.from(schema);
180+
for (let i = 0; i < 5; i++) {
181+
const value = random(type);
182+
expect(() => type.validate(value)).not.toThrow();
183+
}
184+
}
185+
186+
for (const [name, schema] of Object.entries(schemaCategories.composites)) {
187+
const type = t.from(schema);
188+
for (let i = 0; i < 5; i++) {
189+
const value = random(type);
190+
expect(() => type.validate(value)).not.toThrow();
191+
}
192+
}
193+
});
194+
});
195+
196+
describe('comprehensive schema validation', () => {
197+
test('generated values pass validation for all fixture schemas', () => {
198+
for (const [name, schema] of Object.entries(allSchemas)) {
199+
const type = t.from(schema);
200+
201+
// Test multiple random generations for each schema
202+
for (let i = 0; i < 10; i++) {
203+
const randomValue = type.random();
204+
205+
// Test using both validate methods
206+
expect(() => type.validate(randomValue)).not.toThrow();
207+
208+
// Test using compiled validator
209+
const validator = type.compileValidator({errors: 'object'});
210+
const error = validator(randomValue);
211+
expect(error).toBe(null);
212+
}
213+
}
214+
});
215+
216+
test('handles nested complex structures', () => {
217+
const complexType = t.Object(
218+
t.prop(
219+
'users',
220+
t.Array(
221+
t.Object(
222+
t.prop('id', t.Number()),
223+
t.prop(
224+
'profile',
225+
t.Object(t.prop('name', t.String()), t.prop('preferences', t.Map(t.Or(t.String(), t.Boolean())))),
226+
),
227+
t.propOpt('tags', t.Array(t.String())),
228+
),
229+
),
230+
),
231+
t.prop('metadata', t.Map(t.Any())),
232+
t.prop('config', t.Tuple(t.String(), t.Number(), t.Object(t.prop('enabled', t.Boolean())))),
233+
);
234+
235+
for (let i = 0; i < 5; i++) {
236+
const value = complexType.random();
237+
expect(() => complexType.validate(value)).not.toThrow();
238+
}
239+
});
240+
241+
test('handles edge cases and constraints', () => {
242+
// Empty array constraint
243+
const emptyArrayType = t.Array(t.String(), {max: 0});
244+
const emptyArray = emptyArrayType.random();
245+
expect(emptyArray).toEqual([]);
246+
emptyArrayType.validate(emptyArray);
247+
248+
// Single item array constraint
249+
const singleItemType = t.Array(t.Number(), {min: 1, max: 1});
250+
const singleItem = singleItemType.random();
251+
expect(singleItem).toHaveLength(1);
252+
singleItemType.validate(singleItem);
253+
254+
// Number with tight range
255+
const tightRangeType = t.Number({gte: 5, lte: 5});
256+
const tightRangeValue = tightRangeType.random();
257+
expect(tightRangeValue).toBe(5);
258+
tightRangeType.validate(tightRangeValue);
259+
});
260+
});
261+
262+
describe('deterministic behavior with controlled randomness', () => {
263+
test('generates consistent values with mocked Math.random', () => {
264+
const originalRandom = Math.random;
265+
let callCount = 0;
266+
Math.random = () => {
267+
callCount++;
268+
return 0.5; // Always return 0.5 for predictable results
269+
};
270+
271+
try {
272+
const type = t.String({min: 5, max: 5});
273+
const value1 = type.random();
274+
const value2 = type.random();
275+
276+
// With fixed random, string generation should be consistent
277+
expect(value1).toBe(value2);
278+
expect(value1).toHaveLength(5);
279+
type.validate(value1);
280+
} finally {
281+
Math.random = originalRandom;
282+
}
283+
});
284+
});
285+
});

0 commit comments

Comments
 (0)