@reventlessdev/reventless-seed 1.0.0-alpha.2 → 1.0.0-alpha.20
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/CHANGELOG.md +137 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/.sourcedirs.json +1 -0
- package/lib/bs/ReventlessSeed.cmi +0 -0
- package/lib/bs/ReventlessSeed.cmt +0 -0
- package/lib/bs/ReventlessSeed.mlmap +5 -0
- package/lib/bs/compiler-info.json +2 -2
- package/lib/bs/src/Seed-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed.ast +0 -0
- package/lib/bs/src/Seed.res +6 -3
- package/lib/bs/src/Seed.res.mjs +3 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Client.ast +0 -0
- package/lib/bs/src/Seed_Client.res +228 -15
- package/lib/bs/src/Seed_Client.res.mjs +196 -23
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Connect.ast +0 -0
- package/lib/bs/src/Seed_Connect.res +27 -18
- package/lib/bs/src/Seed_Connect.res.mjs +10 -6
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Prompt.ast +0 -0
- package/lib/bs/src/Seed_Prompt.res +110 -35
- package/lib/bs/src/Seed_Prompt.res.mjs +73 -8
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Runner.ast +0 -0
- package/lib/bs/src/Seed_Runner.res +38 -7
- package/lib/bs/src/Seed_Runner.res.mjs +19 -1
- package/lib/bs/src/Seed_Types-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Upload.ast +0 -0
- package/lib/bs/src/Seed_Upload.res +40 -42
- package/lib/bs/src/Seed_Upload.res.mjs +65 -69
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Users.ast +0 -0
- package/lib/bs/src/Seed_Users.res +80 -0
- package/lib/bs/src/Seed_Users.res.mjs +90 -0
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_ClientTest.ast +0 -0
- package/lib/bs/tests/Seed_ClientTest.res +157 -0
- package/lib/bs/tests/Seed_ClientTest.res.mjs +167 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_PromptTest.ast +0 -0
- package/lib/bs/tests/Seed_PromptTest.res +59 -0
- package/lib/bs/tests/Seed_PromptTest.res.mjs +63 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_RandomTest.ast +0 -0
- package/lib/bs/tests/Seed_RandomTest.res +160 -0
- package/lib/bs/tests/Seed_RandomTest.res.mjs +208 -0
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_UsersTest.ast +0 -0
- package/lib/bs/tests/Seed_UsersTest.res +92 -0
- package/lib/bs/tests/Seed_UsersTest.res.mjs +95 -0
- package/lib/ocaml/.compiler.log +2 -2
- package/lib/ocaml/ReventlessSeed.cmi +0 -0
- package/lib/ocaml/ReventlessSeed.cmt +0 -0
- package/lib/ocaml/ReventlessSeed.mlmap +5 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed.ast +0 -0
- package/lib/ocaml/Seed.res +6 -3
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Client.ast +0 -0
- package/lib/ocaml/Seed_Client.res +228 -15
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_ClientTest.ast +0 -0
- package/lib/ocaml/Seed_ClientTest.res +157 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Connect.ast +0 -0
- package/lib/ocaml/Seed_Connect.res +27 -18
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Prompt.ast +0 -0
- package/lib/ocaml/Seed_Prompt.res +110 -35
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_PromptTest.ast +0 -0
- package/lib/ocaml/Seed_PromptTest.res +59 -0
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_RandomTest.ast +0 -0
- package/lib/ocaml/Seed_RandomTest.res +160 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Runner.ast +0 -0
- package/lib/ocaml/Seed_Runner.res +38 -7
- package/lib/ocaml/Seed_Types-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Upload.ast +0 -0
- package/lib/ocaml/Seed_Upload.res +40 -42
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Users.ast +0 -0
- package/lib/ocaml/Seed_Users.res +80 -0
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_UsersTest.ast +0 -0
- package/lib/ocaml/Seed_UsersTest.res +92 -0
- package/package.json +7 -2
- package/rescript.json +9 -1
- package/src/Seed.res +6 -3
- package/src/Seed.res.mjs +3 -0
- package/src/Seed_Client.res +228 -15
- package/src/Seed_Client.res.mjs +196 -23
- package/src/Seed_Connect.res +27 -18
- package/src/Seed_Connect.res.mjs +10 -6
- package/src/Seed_Prompt.res +110 -35
- package/src/Seed_Prompt.res.mjs +73 -8
- package/src/Seed_Runner.res +38 -7
- package/src/Seed_Runner.res.mjs +19 -1
- package/src/Seed_Upload.res +40 -42
- package/src/Seed_Upload.res.mjs +65 -69
- package/src/Seed_Users.res +80 -0
- package/src/Seed_Users.res.mjs +90 -0
- package/tests/Seed_ClientTest.res +157 -0
- package/tests/Seed_ClientTest.res.mjs +167 -0
- package/tests/Seed_PromptTest.res +59 -0
- package/tests/Seed_PromptTest.res.mjs +63 -0
- package/tests/Seed_RandomTest.res +160 -0
- package/tests/Seed_RandomTest.res.mjs +208 -0
- package/tests/Seed_UsersTest.res +92 -0
- package/tests/Seed_UsersTest.res.mjs +95 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// `Seed_Random` exists so that two seed runs against a fresh store produce the
|
|
4
|
+
// same data — that is what makes a seeded store usable as a screenshot baseline
|
|
5
|
+
// or a comparison target. Its correctness is therefore not "looks random" but
|
|
6
|
+
// "repeats exactly, and stays inside the bounds it advertises".
|
|
7
|
+
//
|
|
8
|
+
// The suite is written around that: every case below fails for a generator that
|
|
9
|
+
// is subtly non-reproducible or that walks out of its range, and none of them
|
|
10
|
+
// would notice a change of algorithm that kept both properties.
|
|
11
|
+
|
|
12
|
+
describe("Seed_Random:", () => {
|
|
13
|
+
let take = (~seed, ~n) => {
|
|
14
|
+
let r = Seed_Random.make(~seed)
|
|
15
|
+
Array.make(~length=n, 0.0)->Array.map(_ => Seed_Random.float(r))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("reproducibility — the reason this module exists:", () => {
|
|
19
|
+
testSync("the same seed replays the same sequence", () =>
|
|
20
|
+
expect(take(~seed=42, ~n=25))->toEqual(take(~seed=42, ~n=25))
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
// Without this, a generator that returned a constant would satisfy the
|
|
24
|
+
// case above and nothing else in the suite would catch it.
|
|
25
|
+
testSync("different seeds diverge", () =>
|
|
26
|
+
expect(take(~seed=42, ~n=25) == take(~seed=43, ~n=25))->toBe(false)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
testSync("a fresh generator does not continue the previous one", () => {
|
|
30
|
+
let r = Seed_Random.make(~seed=7)
|
|
31
|
+
let first = Seed_Random.float(r)
|
|
32
|
+
let _ = Seed_Random.float(r)
|
|
33
|
+
expect(Seed_Random.float(Seed_Random.make(~seed=7)))->toEqual(first)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe("float stays a unit interval:", () => {
|
|
38
|
+
// mulberry32's last step is a signed 32-bit value folded back to unsigned.
|
|
39
|
+
// Get that fold wrong and roughly half the draws come out negative, which
|
|
40
|
+
// every consumer below (int, pick, sampleWeighted) silently mis-scales.
|
|
41
|
+
testSync("every draw is in [0, 1)", () =>
|
|
42
|
+
expect(take(~seed=99, ~n=500)->Array.every(f => f >= 0.0 && f < 1.0))->toBe(true)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
testSync("and the draws are not all the same value", () => {
|
|
46
|
+
let draws = take(~seed=99, ~n=100)
|
|
47
|
+
expect(draws->Array.some(f => f != draws->Array.getUnsafe(0)))->toBe(true)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe("int is inclusive on both ends, as documented:", () => {
|
|
52
|
+
let draws = (~min, ~max, ~n) => {
|
|
53
|
+
let r = Seed_Random.make(~seed=5)
|
|
54
|
+
Array.make(~length=n, 0)->Array.map(_ => Seed_Random.int(r, ~min, ~max))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
testSync("never escapes the range", () =>
|
|
58
|
+
expect(draws(~min=3, ~max=7, ~n=500)->Array.every(i => i >= 3 && i <= 7))->toBe(true)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// The documented contract is "inclusive on both ends"; an off-by-one in the
|
|
62
|
+
// `max - min + 1` scaling shows up as a bound that is never drawn.
|
|
63
|
+
testSync("reaches both bounds", () => {
|
|
64
|
+
let d = draws(~min=3, ~max=7, ~n=500)
|
|
65
|
+
expect((d->Array.includes(3), d->Array.includes(7)))->toEqual((true, true))
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
testSync("a single-value range is that value", () =>
|
|
69
|
+
expect(draws(~min=4, ~max=4, ~n=10)->Array.every(i => i == 4))->toBe(true)
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe("pick:", () => {
|
|
74
|
+
testSync("returns None for an empty array", () =>
|
|
75
|
+
expect(Seed_Random.make(~seed=1)->Seed_Random.pick([]))->toEqual(None)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
testSync("always lands on a member", () => {
|
|
79
|
+
let r = Seed_Random.make(~seed=2)
|
|
80
|
+
let xs = ["a", "b", "c", "d"]
|
|
81
|
+
let hits = Array.make(~length=200, "")->Array.map(_ =>
|
|
82
|
+
r->Seed_Random.pickOr(~fallback="MISS", xs)
|
|
83
|
+
)
|
|
84
|
+
expect(hits->Array.every(h => xs->Array.includes(h)))->toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
testSync("pickOr falls back only when there is nothing to pick", () =>
|
|
88
|
+
expect(Seed_Random.make(~seed=3)->Seed_Random.pickOr(~fallback="fallback", []))->toBe(
|
|
89
|
+
"fallback",
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe("sampleWeighted is without replacement:", () => {
|
|
95
|
+
let equal = xs => xs->Array.map(x => (x, 1.0))
|
|
96
|
+
|
|
97
|
+
// The documented shuffle use: equal weights, count = length. If sampling
|
|
98
|
+
// ever replaced instead of removing, duplicates would appear here.
|
|
99
|
+
testSync("count = length yields a permutation, not a multiset", () => {
|
|
100
|
+
let xs = ["a", "b", "c", "d", "e"]
|
|
101
|
+
let out = Seed_Random.make(~seed=11)->Seed_Random.sampleWeighted(equal(xs), ~count=5)
|
|
102
|
+
let sorted = out->Array.toSorted(String.compare)
|
|
103
|
+
expect((out->Array.length, sorted))->toEqual((5, ["a", "b", "c", "d", "e"]))
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
testSync("count < length returns exactly count distinct items", () => {
|
|
107
|
+
let xs = ["a", "b", "c", "d", "e"]
|
|
108
|
+
let out = Seed_Random.make(~seed=12)->Seed_Random.sampleWeighted(equal(xs), ~count=3)
|
|
109
|
+
let distinct = Set.fromArray(out)->Set.size
|
|
110
|
+
expect((out->Array.length, distinct))->toEqual((3, 3))
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
testSync("count > length clamps to the pool", () =>
|
|
114
|
+
expect(
|
|
115
|
+
Seed_Random.make(~seed=13)
|
|
116
|
+
->Seed_Random.sampleWeighted(equal(["a", "b"]), ~count=99)
|
|
117
|
+
->Array.length,
|
|
118
|
+
)->toBe(2)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
testSync("a zero-weight entry is never chosen while others remain", () => {
|
|
122
|
+
let out =
|
|
123
|
+
Seed_Random.make(~seed=14)->Seed_Random.sampleWeighted(
|
|
124
|
+
[("never", 0.0), ("a", 1.0), ("b", 1.0)],
|
|
125
|
+
~count=2,
|
|
126
|
+
)
|
|
127
|
+
expect(out->Array.includes("never"))->toBe(false)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
testSync("is reproducible for a given seed", () => {
|
|
131
|
+
let run = () =>
|
|
132
|
+
Seed_Random.make(~seed=15)->Seed_Random.sampleWeighted(
|
|
133
|
+
equal(["a", "b", "c", "d", "e", "f"]),
|
|
134
|
+
~count=4,
|
|
135
|
+
)
|
|
136
|
+
expect(run())->toEqual(run())
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe("zipfWeights skews toward the head:", () => {
|
|
141
|
+
let weights = Seed_Random.zipfWeights(["a", "b", "c", "d", "e"], ~exponent=1.1)
|
|
142
|
+
|
|
143
|
+
testSync("keeps every item, in order", () =>
|
|
144
|
+
expect(weights->Array.map(((item, _)) => item))->toEqual(["a", "b", "c", "d", "e"])
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
testSync("weight decreases monotonically with rank", () => {
|
|
148
|
+
let ws = weights->Array.map(((_, w)) => w)
|
|
149
|
+
let decreasing =
|
|
150
|
+
ws->Array.everyWithIndex((w, i) => i == 0 || w < ws->Array.getUnsafe(i - 1))
|
|
151
|
+
expect(decreasing)->toBe(true)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
testSync("the leader is clear — first outweighs last several times over", () => {
|
|
155
|
+
let first = weights->Array.get(0)->Option.mapOr(0.0, ((_, w)) => w)
|
|
156
|
+
let last = weights->Array.get(4)->Option.mapOr(0.0, ((_, w)) => w)
|
|
157
|
+
expect(first > last *. 3.0)->toBe(true)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
7
|
+
import * as Seed_Random$ReventlessSeed from "../src/Seed_Random.res.mjs";
|
|
8
|
+
|
|
9
|
+
globalThis.describe("Seed_Random:", () => {
|
|
10
|
+
let take = (seed, n) => {
|
|
11
|
+
let r = Seed_Random$ReventlessSeed.make(seed);
|
|
12
|
+
return Stdlib_Array.make(n, 0.0).map(param => Seed_Random$ReventlessSeed.float(r));
|
|
13
|
+
};
|
|
14
|
+
globalThis.describe("reproducibility — the reason this module exists:", () => {
|
|
15
|
+
globalThis.test("the same seed replays the same sequence", () => {
|
|
16
|
+
globalThis.expect(take(42, 25)).toEqual(take(42, 25));
|
|
17
|
+
});
|
|
18
|
+
globalThis.test("different seeds diverge", () => {
|
|
19
|
+
globalThis.expect(Primitive_object.equal(take(42, 25), take(43, 25))).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
globalThis.test("a fresh generator does not continue the previous one", () => {
|
|
22
|
+
let r = Seed_Random$ReventlessSeed.make(7);
|
|
23
|
+
let first = Seed_Random$ReventlessSeed.float(r);
|
|
24
|
+
Seed_Random$ReventlessSeed.float(r);
|
|
25
|
+
globalThis.expect(Seed_Random$ReventlessSeed.float(Seed_Random$ReventlessSeed.make(7))).toEqual(first);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
globalThis.describe("float stays a unit interval:", () => {
|
|
29
|
+
globalThis.test("every draw is in [0, 1)", () => {
|
|
30
|
+
globalThis.expect(take(99, 500).every(f => {
|
|
31
|
+
if (f >= 0.0) {
|
|
32
|
+
return f < 1.0;
|
|
33
|
+
} else {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
})).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
globalThis.test("and the draws are not all the same value", () => {
|
|
39
|
+
let draws = take(99, 100);
|
|
40
|
+
globalThis.expect(draws.some(f => f !== draws[0])).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
globalThis.describe("int is inclusive on both ends, as documented:", () => {
|
|
44
|
+
let draws = (min, max, n) => {
|
|
45
|
+
let r = Seed_Random$ReventlessSeed.make(5);
|
|
46
|
+
return Stdlib_Array.make(n, 0).map(param => Seed_Random$ReventlessSeed.int(r, min, max));
|
|
47
|
+
};
|
|
48
|
+
globalThis.test("never escapes the range", () => {
|
|
49
|
+
globalThis.expect(draws(3, 7, 500).every(i => {
|
|
50
|
+
if (i >= 3) {
|
|
51
|
+
return i <= 7;
|
|
52
|
+
} else {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
})).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
globalThis.test("reaches both bounds", () => {
|
|
58
|
+
let d = draws(3, 7, 500);
|
|
59
|
+
globalThis.expect([
|
|
60
|
+
d.includes(3),
|
|
61
|
+
d.includes(7)
|
|
62
|
+
]).toEqual([
|
|
63
|
+
true,
|
|
64
|
+
true
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
globalThis.test("a single-value range is that value", () => {
|
|
68
|
+
globalThis.expect(draws(4, 4, 10).every(i => i === 4)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
globalThis.describe("pick:", () => {
|
|
72
|
+
globalThis.test("returns None for an empty array", () => {
|
|
73
|
+
globalThis.expect(Seed_Random$ReventlessSeed.pick(Seed_Random$ReventlessSeed.make(1), [])).toEqual(undefined);
|
|
74
|
+
});
|
|
75
|
+
globalThis.test("always lands on a member", () => {
|
|
76
|
+
let r = Seed_Random$ReventlessSeed.make(2);
|
|
77
|
+
let xs = [
|
|
78
|
+
"a",
|
|
79
|
+
"b",
|
|
80
|
+
"c",
|
|
81
|
+
"d"
|
|
82
|
+
];
|
|
83
|
+
let hits = Stdlib_Array.make(200, "").map(param => Seed_Random$ReventlessSeed.pickOr(r, "MISS", xs));
|
|
84
|
+
globalThis.expect(hits.every(h => xs.includes(h))).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
globalThis.test("pickOr falls back only when there is nothing to pick", () => {
|
|
87
|
+
globalThis.expect(Seed_Random$ReventlessSeed.pickOr(Seed_Random$ReventlessSeed.make(3), "fallback", [])).toBe("fallback");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
globalThis.describe("sampleWeighted is without replacement:", () => {
|
|
91
|
+
let equal = xs => xs.map(x => [
|
|
92
|
+
x,
|
|
93
|
+
1.0
|
|
94
|
+
]);
|
|
95
|
+
globalThis.test("count = length yields a permutation, not a multiset", () => {
|
|
96
|
+
let xs = [
|
|
97
|
+
"a",
|
|
98
|
+
"b",
|
|
99
|
+
"c",
|
|
100
|
+
"d",
|
|
101
|
+
"e"
|
|
102
|
+
];
|
|
103
|
+
let out = Seed_Random$ReventlessSeed.sampleWeighted(Seed_Random$ReventlessSeed.make(11), equal(xs), 5);
|
|
104
|
+
let sorted = out.toSorted(Primitive_string.compare);
|
|
105
|
+
globalThis.expect([
|
|
106
|
+
out.length,
|
|
107
|
+
sorted
|
|
108
|
+
]).toEqual([
|
|
109
|
+
5,
|
|
110
|
+
[
|
|
111
|
+
"a",
|
|
112
|
+
"b",
|
|
113
|
+
"c",
|
|
114
|
+
"d",
|
|
115
|
+
"e"
|
|
116
|
+
]
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
globalThis.test("count < length returns exactly count distinct items", () => {
|
|
120
|
+
let xs = [
|
|
121
|
+
"a",
|
|
122
|
+
"b",
|
|
123
|
+
"c",
|
|
124
|
+
"d",
|
|
125
|
+
"e"
|
|
126
|
+
];
|
|
127
|
+
let out = Seed_Random$ReventlessSeed.sampleWeighted(Seed_Random$ReventlessSeed.make(12), equal(xs), 3);
|
|
128
|
+
let distinct = new Set(out).size;
|
|
129
|
+
globalThis.expect([
|
|
130
|
+
out.length,
|
|
131
|
+
distinct
|
|
132
|
+
]).toEqual([
|
|
133
|
+
3,
|
|
134
|
+
3
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
globalThis.test("count > length clamps to the pool", () => {
|
|
138
|
+
globalThis.expect(Seed_Random$ReventlessSeed.sampleWeighted(Seed_Random$ReventlessSeed.make(13), equal([
|
|
139
|
+
"a",
|
|
140
|
+
"b"
|
|
141
|
+
]), 99).length).toBe(2);
|
|
142
|
+
});
|
|
143
|
+
globalThis.test("a zero-weight entry is never chosen while others remain", () => {
|
|
144
|
+
let out = Seed_Random$ReventlessSeed.sampleWeighted(Seed_Random$ReventlessSeed.make(14), [
|
|
145
|
+
[
|
|
146
|
+
"never",
|
|
147
|
+
0.0
|
|
148
|
+
],
|
|
149
|
+
[
|
|
150
|
+
"a",
|
|
151
|
+
1.0
|
|
152
|
+
],
|
|
153
|
+
[
|
|
154
|
+
"b",
|
|
155
|
+
1.0
|
|
156
|
+
]
|
|
157
|
+
], 2);
|
|
158
|
+
globalThis.expect(out.includes("never")).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
globalThis.test("is reproducible for a given seed", () => {
|
|
161
|
+
let run = () => Seed_Random$ReventlessSeed.sampleWeighted(Seed_Random$ReventlessSeed.make(15), equal([
|
|
162
|
+
"a",
|
|
163
|
+
"b",
|
|
164
|
+
"c",
|
|
165
|
+
"d",
|
|
166
|
+
"e",
|
|
167
|
+
"f"
|
|
168
|
+
]), 4);
|
|
169
|
+
globalThis.expect(run()).toEqual(run());
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
globalThis.describe("zipfWeights skews toward the head:", () => {
|
|
173
|
+
let weights = Seed_Random$ReventlessSeed.zipfWeights([
|
|
174
|
+
"a",
|
|
175
|
+
"b",
|
|
176
|
+
"c",
|
|
177
|
+
"d",
|
|
178
|
+
"e"
|
|
179
|
+
], 1.1);
|
|
180
|
+
globalThis.test("keeps every item, in order", () => {
|
|
181
|
+
globalThis.expect(weights.map(param => param[0])).toEqual([
|
|
182
|
+
"a",
|
|
183
|
+
"b",
|
|
184
|
+
"c",
|
|
185
|
+
"d",
|
|
186
|
+
"e"
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
globalThis.test("weight decreases monotonically with rank", () => {
|
|
190
|
+
let ws = weights.map(param => param[1]);
|
|
191
|
+
let decreasing = ws.every((w, i) => {
|
|
192
|
+
if (i === 0) {
|
|
193
|
+
return true;
|
|
194
|
+
} else {
|
|
195
|
+
return w < ws[i - 1 | 0];
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
globalThis.expect(decreasing).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
globalThis.test("the leader is clear — first outweighs last several times over", () => {
|
|
201
|
+
let first = Stdlib_Option.mapOr(weights[0], 0.0, param => param[1]);
|
|
202
|
+
let last = Stdlib_Option.mapOr(weights[4], 0.0, param => param[1]);
|
|
203
|
+
globalThis.expect(first > last * 3.0).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
/* Not a pure module */
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// `.reventless/users.yaml` is hand-maintained and shared with the auth adapter,
|
|
4
|
+
// so the seed reads it defensively: it must survive the comment blocks both
|
|
5
|
+
// copies carry, must not reorder the accounts (the first one is the prompt's
|
|
6
|
+
// default, so order IS the contract), and must never throw — a broken file costs
|
|
7
|
+
// the menu, not the run.
|
|
8
|
+
|
|
9
|
+
let document = `
|
|
10
|
+
# Local dev users — a comment block like the real file carries.
|
|
11
|
+
|
|
12
|
+
- username: admin
|
|
13
|
+
password: admin
|
|
14
|
+
groups: [Admin, Shopper]
|
|
15
|
+
userId: local-admin
|
|
16
|
+
|
|
17
|
+
- username: shopper
|
|
18
|
+
password: Shopper-2026!
|
|
19
|
+
groups: [Shopper]
|
|
20
|
+
|
|
21
|
+
- username: noGroups
|
|
22
|
+
password: pw
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
describe("Seed_Users.parseString:", () => {
|
|
26
|
+
testSync("reads every account, in the order the file defines them", () => {
|
|
27
|
+
let users = Seed_Users.parseString(document)
|
|
28
|
+
expect(users->Array.map(u => u.Seed_Users.username))->Expect.toEqual([
|
|
29
|
+
"admin",
|
|
30
|
+
"shopper",
|
|
31
|
+
"noGroups",
|
|
32
|
+
])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
testSync("carries the password beside the username", () => {
|
|
36
|
+
let users = Seed_Users.parseString(document)
|
|
37
|
+
expect(users->Array.map(u => u.Seed_Users.password))->Expect.toEqual([
|
|
38
|
+
"admin",
|
|
39
|
+
"Shopper-2026!",
|
|
40
|
+
"pw",
|
|
41
|
+
])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
testSync("keeps the groups, and reads a missing groups key as none", () => {
|
|
45
|
+
let users = Seed_Users.parseString(document)
|
|
46
|
+
expect(users->Array.map(u => u.Seed_Users.groups))->Expect.toEqual([
|
|
47
|
+
["Admin", "Shopper"],
|
|
48
|
+
["Shopper"],
|
|
49
|
+
[],
|
|
50
|
+
])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// An entry half-written by hand is the likely flaw in this file. Dropping it
|
|
54
|
+
// keeps the other accounts selectable; failing the parse would not.
|
|
55
|
+
testSync("skips an entry that names no password", () => {
|
|
56
|
+
let users = Seed_Users.parseString(`
|
|
57
|
+
- username: admin
|
|
58
|
+
password: admin
|
|
59
|
+
- username: halfWritten
|
|
60
|
+
groups: [Shopper]
|
|
61
|
+
`)
|
|
62
|
+
expect(users->Array.map(u => u.Seed_Users.username))->Expect.toEqual(["admin"])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
testSync("yields no accounts for a document that is not a list", () => {
|
|
66
|
+
expect(Seed_Users.parseString("users: {}")->Array.length)->Expect.toBe(0)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// The whole point of returning accounts rather than raising: the caller falls
|
|
70
|
+
// back to prompting, and it can only do that if nothing here escapes.
|
|
71
|
+
testSync("yields no accounts for malformed YAML instead of throwing", () => {
|
|
72
|
+
expect(Seed_Users.parseString("- username: [unclosed")->Array.length)->Expect.toBe(0)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe("Seed_Users.load:", () => {
|
|
77
|
+
testSync("is silent about a file that is not there", () => {
|
|
78
|
+
expect(Seed_Users.load(~path="/nonexistent/.reventless/users.yaml"))->Expect.toBe(None)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe("Seed_Users.label:", () => {
|
|
83
|
+
testSync("names the groups an account carries", () => {
|
|
84
|
+
expect(
|
|
85
|
+
Seed_Users.label({username: "admin", password: "x", groups: ["Admin", "Shopper"]}),
|
|
86
|
+
)->Expect.toBe("admin [Admin, Shopper]")
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
testSync("is the bare username when the file records no groups", () => {
|
|
90
|
+
expect(Seed_Users.label({username: "admin", password: "x", groups: []}))->Expect.toBe("admin")
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Seed_Users$ReventlessSeed from "../src/Seed_Users.res.mjs";
|
|
4
|
+
|
|
5
|
+
let document = `
|
|
6
|
+
# Local dev users — a comment block like the real file carries.
|
|
7
|
+
|
|
8
|
+
- username: admin
|
|
9
|
+
password: admin
|
|
10
|
+
groups: [Admin, Shopper]
|
|
11
|
+
userId: local-admin
|
|
12
|
+
|
|
13
|
+
- username: shopper
|
|
14
|
+
password: Shopper-2026!
|
|
15
|
+
groups: [Shopper]
|
|
16
|
+
|
|
17
|
+
- username: noGroups
|
|
18
|
+
password: pw
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
globalThis.describe("Seed_Users.parseString:", () => {
|
|
22
|
+
globalThis.test("reads every account, in the order the file defines them", () => {
|
|
23
|
+
let users = Seed_Users$ReventlessSeed.parseString(document);
|
|
24
|
+
globalThis.expect(users.map(u => u.username)).toEqual([
|
|
25
|
+
"admin",
|
|
26
|
+
"shopper",
|
|
27
|
+
"noGroups"
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
globalThis.test("carries the password beside the username", () => {
|
|
31
|
+
let users = Seed_Users$ReventlessSeed.parseString(document);
|
|
32
|
+
globalThis.expect(users.map(u => u.password)).toEqual([
|
|
33
|
+
"admin",
|
|
34
|
+
"Shopper-2026!",
|
|
35
|
+
"pw"
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
globalThis.test("keeps the groups, and reads a missing groups key as none", () => {
|
|
39
|
+
let users = Seed_Users$ReventlessSeed.parseString(document);
|
|
40
|
+
globalThis.expect(users.map(u => u.groups)).toEqual([
|
|
41
|
+
[
|
|
42
|
+
"Admin",
|
|
43
|
+
"Shopper"
|
|
44
|
+
],
|
|
45
|
+
["Shopper"],
|
|
46
|
+
[]
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
globalThis.test("skips an entry that names no password", () => {
|
|
50
|
+
let users = Seed_Users$ReventlessSeed.parseString(`
|
|
51
|
+
- username: admin
|
|
52
|
+
password: admin
|
|
53
|
+
- username: halfWritten
|
|
54
|
+
groups: [Shopper]
|
|
55
|
+
`);
|
|
56
|
+
globalThis.expect(users.map(u => u.username)).toEqual(["admin"]);
|
|
57
|
+
});
|
|
58
|
+
globalThis.test("yields no accounts for a document that is not a list", () => {
|
|
59
|
+
globalThis.expect(Seed_Users$ReventlessSeed.parseString("users: {}").length).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
globalThis.test("yields no accounts for malformed YAML instead of throwing", () => {
|
|
62
|
+
globalThis.expect(Seed_Users$ReventlessSeed.parseString("- username: [unclosed").length).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
globalThis.describe("Seed_Users.load:", () => {
|
|
67
|
+
globalThis.test("is silent about a file that is not there", () => {
|
|
68
|
+
globalThis.expect(Seed_Users$ReventlessSeed.load("/nonexistent/.reventless/users.yaml")).toBe(undefined);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
globalThis.describe("Seed_Users.label:", () => {
|
|
73
|
+
globalThis.test("names the groups an account carries", () => {
|
|
74
|
+
globalThis.expect(Seed_Users$ReventlessSeed.label({
|
|
75
|
+
username: "admin",
|
|
76
|
+
password: "x",
|
|
77
|
+
groups: [
|
|
78
|
+
"Admin",
|
|
79
|
+
"Shopper"
|
|
80
|
+
]
|
|
81
|
+
})).toBe("admin [Admin, Shopper]");
|
|
82
|
+
});
|
|
83
|
+
globalThis.test("is the bare username when the file records no groups", () => {
|
|
84
|
+
globalThis.expect(Seed_Users$ReventlessSeed.label({
|
|
85
|
+
username: "admin",
|
|
86
|
+
password: "x",
|
|
87
|
+
groups: []
|
|
88
|
+
})).toBe("admin");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export {
|
|
93
|
+
document,
|
|
94
|
+
}
|
|
95
|
+
/* Not a pure module */
|
package/lib/ocaml/.compiler.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#Start(
|
|
2
|
-
#Done(
|
|
1
|
+
#Start(1788869322834)
|
|
2
|
+
#Done(1788869322875)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/ocaml/Seed.ast
CHANGED
|
Binary file
|
package/lib/ocaml/Seed.res
CHANGED
|
@@ -10,15 +10,17 @@ module Client = Seed_Client
|
|
|
10
10
|
module Runner = Seed_Runner
|
|
11
11
|
module Upload = Seed_Upload
|
|
12
12
|
module Prompt = Seed_Prompt
|
|
13
|
+
module Users = Seed_Users
|
|
13
14
|
module Connect = Seed_Connect
|
|
14
15
|
|
|
15
16
|
exception Failed = Seed_Types.Failed
|
|
16
17
|
|
|
17
|
-
// A live target a data set seeds against: an authenticated client,
|
|
18
|
-
//
|
|
18
|
+
// A live target a data set seeds against: an authenticated client, whether uploads
|
|
19
|
+
// are skipped this run, and a label. Uploads mint through the domain API's
|
|
20
|
+
// `Upload_Presign` mutation on the client (route B), so no upload endpoint is carried.
|
|
19
21
|
type connection = Seed_Connect.connection = {
|
|
20
22
|
client: Seed_Client.t,
|
|
21
|
-
|
|
23
|
+
uploadsSkipped: bool,
|
|
22
24
|
label: string,
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -27,6 +29,7 @@ type dataSet = Seed_Runner.dataSet = {
|
|
|
27
29
|
name: string,
|
|
28
30
|
label: string,
|
|
29
31
|
seed: connection => promise<unit>,
|
|
32
|
+
probeViews?: array<string>,
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
type rec value = Seed_Types.value =
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|