@reventlessdev/reventless-seed 1.0.0-alpha.20 → 1.0.0-alpha.22
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 +19 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/ReventlessSeed.cmi +0 -0
- package/lib/bs/ReventlessSeed.cmt +0 -0
- package/lib/bs/ReventlessSeed.mlmap +1 -0
- package/lib/bs/compiler-info.json +1 -1
- 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 +8 -2
- 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 +169 -21
- package/lib/bs/src/Seed_Client.res.mjs +142 -7
- 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 +258 -19
- package/lib/bs/src/Seed_Connect.res.mjs +158 -8
- 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 +48 -23
- package/lib/bs/src/Seed_Prompt.res.mjs +33 -22
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Random.ast +0 -0
- package/lib/bs/src/Seed_Random.res +4 -2
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +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 +7 -12
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +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 +4 -2
- 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 +30 -3
- package/lib/bs/src/Seed_Users.res.mjs +14 -1
- 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 +146 -16
- package/lib/bs/tests/Seed_ClientTest.res.mjs +102 -1
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_ConnectTest.ast +0 -0
- package/lib/bs/tests/Seed_ConnectTest.res +64 -0
- package/lib/bs/tests/Seed_ConnectTest.res.mjs +56 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +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 +8 -12
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmi +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 +130 -90
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmi +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 +77 -2
- package/lib/bs/tests/Seed_UsersTest.res.mjs +45 -2
- 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 +1 -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 +8 -2
- 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 +169 -21
- 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 +146 -16
- 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 +258 -19
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_ConnectTest.ast +0 -0
- package/lib/ocaml/Seed_ConnectTest.res +64 -0
- 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 +48 -23
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +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 +8 -12
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Random.ast +0 -0
- package/lib/ocaml/Seed_Random.res +4 -2
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmi +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 +130 -90
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +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 +7 -12
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +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 +4 -2
- 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 +30 -3
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmi +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 +77 -2
- package/package.json +2 -2
- package/src/Seed.res +8 -2
- package/src/Seed_Client.res +169 -21
- package/src/Seed_Client.res.mjs +142 -7
- package/src/Seed_Connect.res +258 -19
- package/src/Seed_Connect.res.mjs +158 -8
- package/src/Seed_Prompt.res +48 -23
- package/src/Seed_Prompt.res.mjs +33 -22
- package/src/Seed_Random.res +4 -2
- package/src/Seed_Runner.res +7 -12
- package/src/Seed_Upload.res +4 -2
- package/src/Seed_Users.res +30 -3
- package/src/Seed_Users.res.mjs +14 -1
- package/tests/Seed_ClientTest.res +146 -16
- package/tests/Seed_ClientTest.res.mjs +102 -1
- package/tests/Seed_ConnectTest.res +64 -0
- package/tests/Seed_ConnectTest.res.mjs +56 -0
- package/tests/Seed_PromptTest.res +8 -12
- package/tests/Seed_RandomTest.res +130 -90
- package/tests/Seed_UsersTest.res +77 -2
- package/tests/Seed_UsersTest.res.mjs +45 -2
|
@@ -63,12 +63,16 @@ let envValue = (key: string): option<string> =>
|
|
|
63
63
|
| _ => None
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** Whether an interactive prompt is possible at all. Callers that can carry on
|
|
67
|
+
without an answer ask this first; a prompt nobody can answer is worse than a
|
|
68
|
+
default, because it turns a workable run into a hang or a throw. */
|
|
69
|
+
let hasTty = (): bool => NodeProcess.stdin->NodeProcess.isTTY->Option.getOr(false)
|
|
70
|
+
|
|
66
71
|
let requireTty = (): unit =>
|
|
67
|
-
if !(
|
|
72
|
+
if !hasTty() {
|
|
68
73
|
throw(
|
|
69
74
|
Failed(
|
|
70
|
-
"no TTY for an interactive prompt — set the documented SEED_* / REVENTLESS_DEMO_* " ++
|
|
71
|
-
"env vars to run non-interactively.",
|
|
75
|
+
"no TTY for an interactive prompt — set the documented SEED_* / REVENTLESS_DEMO_* " ++ "env vars to run non-interactively.",
|
|
72
76
|
),
|
|
73
77
|
)
|
|
74
78
|
}
|
|
@@ -125,12 +129,7 @@ let isAffirmative = (answer: string): bool =>
|
|
|
125
129
|
* `defaultIndex` makes Enter a valid answer, selecting that 0-based option;
|
|
126
130
|
* without one every reply must name a choice.
|
|
127
131
|
*/
|
|
128
|
-
let select = async (
|
|
129
|
-
~title: string,
|
|
130
|
-
~options: array<(string, 'a)>,
|
|
131
|
-
~env=?,
|
|
132
|
-
~defaultIndex=?,
|
|
133
|
-
): 'a =>
|
|
132
|
+
let select = async (~title: string, ~options: array<(string, 'a)>, ~env=?, ~defaultIndex=?): 'a =>
|
|
134
133
|
switch options {
|
|
135
134
|
| [(_, only)] => only
|
|
136
135
|
| _ =>
|
|
@@ -163,9 +162,7 @@ let select = async (
|
|
|
163
162
|
| _ => ""
|
|
164
163
|
}
|
|
165
164
|
let rec pick = async (): 'a => {
|
|
166
|
-
let answer = await ask(
|
|
167
|
-
`\nSelect [1-${(options->Array.length)->Int.toString}]${hint}: `,
|
|
168
|
-
)
|
|
165
|
+
let answer = await ask(`\nSelect [1-${options->Array.length->Int.toString}]${hint}: `)
|
|
169
166
|
switch (answer, fallback) {
|
|
170
167
|
| ("", Some((_, value))) => value
|
|
171
168
|
| _ =>
|
|
@@ -183,11 +180,31 @@ let select = async (
|
|
|
183
180
|
}
|
|
184
181
|
}
|
|
185
182
|
|
|
183
|
+
/**
|
|
184
|
+
* The account a run authenticated as, alongside every account the platform's
|
|
185
|
+
* accounts file declares.
|
|
186
|
+
*
|
|
187
|
+
* `accounts` is `[]` whenever no file was read — the
|
|
188
|
+
* `REVENTLESS_DEMO_USER`/`REVENTLESS_DEMO_PASSWORD` path bypasses it entirely,
|
|
189
|
+
* and so does typing both halves — so a consumer must handle empty rather than
|
|
190
|
+
* assume the file was there. `caller` is always populated, but carries no
|
|
191
|
+
* `groups` and no `userId` on those paths: nothing read them off a file.
|
|
192
|
+
*/
|
|
193
|
+
type resolved = {caller: Seed_Users.user, accounts: array<Seed_Users.user>}
|
|
194
|
+
|
|
195
|
+
let typedUser = (~username: string, ~password: string): Seed_Users.user => {
|
|
196
|
+
username,
|
|
197
|
+
password,
|
|
198
|
+
groups: [],
|
|
199
|
+
userId: None,
|
|
200
|
+
demoOwner: None,
|
|
201
|
+
}
|
|
202
|
+
|
|
186
203
|
// Typing both halves is the fallback, not the first offer: it is what happens
|
|
187
204
|
// when the platform keeps no accounts file. `envUser` is threaded in so a
|
|
188
205
|
// `REVENTLESS_DEMO_USER` that named nobody in the file still skips the username
|
|
189
206
|
// prompt rather than asking for a name that was already given.
|
|
190
|
-
let askCredentials = async (~localDefaults: bool, ~envUser: option<string>):
|
|
207
|
+
let askCredentials = async (~localDefaults: bool, ~envUser: option<string>): Seed_Users.user => {
|
|
191
208
|
let username = switch envUser {
|
|
192
209
|
| Some(u) => u
|
|
193
210
|
| None =>
|
|
@@ -202,7 +219,7 @@ let askCredentials = async (~localDefaults: bool, ~envUser: option<string>): (st
|
|
|
202
219
|
let entered = await askHidden(localDefaults ? "Password [admin]: " : "Password: ")
|
|
203
220
|
entered == "" && localDefaults ? "admin" : entered
|
|
204
221
|
}
|
|
205
|
-
(username, password)
|
|
222
|
+
typedUser(~username, ~password)
|
|
206
223
|
}
|
|
207
224
|
|
|
208
225
|
// The accounts file the platform already keeps (`.reventless/users.yaml`, or
|
|
@@ -212,7 +229,12 @@ let askCredentials = async (~localDefaults: bool, ~envUser: option<string>): (st
|
|
|
212
229
|
// A named `REVENTLESS_DEMO_USER` picks its entry directly; otherwise the file's
|
|
213
230
|
// accounts are offered in the order it defines them, first as the default.
|
|
214
231
|
// `None` means the file has nothing to offer and the caller should ask.
|
|
215
|
-
|
|
232
|
+
//
|
|
233
|
+
// The whole list travels back with the chosen account, not just the credentials
|
|
234
|
+
// it supplied: the file is also the only place a demo owner's id can be looked
|
|
235
|
+
// up, and reading it once to pick a login and never again is what let a data set
|
|
236
|
+
// seed rows under ids nobody on the platform holds.
|
|
237
|
+
let fromUsersFile = async (~envUser: option<string>): option<resolved> =>
|
|
216
238
|
switch Seed_Users.load(~path=?envValue("SEED_USERS_FILE")) {
|
|
217
239
|
| None => None
|
|
218
240
|
| Some((path, users)) =>
|
|
@@ -233,7 +255,7 @@ let fromUsersFile = async (~envUser: option<string>): option<(string, string)> =
|
|
|
233
255
|
// operator checks when owner-scoped rows turn up under the wrong account.
|
|
234
256
|
chosen->Option.map(u => {
|
|
235
257
|
Console.log(`Logging in as ${u.username} (from ${path})`)
|
|
236
|
-
|
|
258
|
+
{caller: u, accounts: users}
|
|
237
259
|
})
|
|
238
260
|
}
|
|
239
261
|
|
|
@@ -245,19 +267,22 @@ let fromUsersFile = async (~envUser: option<string>): option<(string, string)> =
|
|
|
245
267
|
* `.reventless/users.yaml` supplies the accounts to choose from, and only a
|
|
246
268
|
* platform without one falls back to typing both halves — where, with
|
|
247
269
|
* `localDefaults`, empty input means `admin`/`admin`.
|
|
270
|
+
*
|
|
271
|
+
* Returns the resolved account rather than a pair, and the accounts file
|
|
272
|
+
* alongside it when there was one — see `resolved`.
|
|
248
273
|
*/
|
|
249
|
-
let credentials = async (~localDefaults: bool=false):
|
|
274
|
+
let credentials = async (~localDefaults: bool=false): resolved => {
|
|
250
275
|
let envUser = envValue("REVENTLESS_DEMO_USER")
|
|
251
|
-
let
|
|
252
|
-
| (Some(
|
|
276
|
+
let resolved = switch (envUser, envValue("REVENTLESS_DEMO_PASSWORD")) {
|
|
277
|
+
| (Some(username), Some(password)) => {caller: typedUser(~username, ~password), accounts: []}
|
|
253
278
|
| _ =>
|
|
254
279
|
switch await fromUsersFile(~envUser) {
|
|
255
|
-
| Some(
|
|
256
|
-
| None => await askCredentials(~localDefaults, ~envUser)
|
|
280
|
+
| Some(resolved) => resolved
|
|
281
|
+
| None => {caller: await askCredentials(~localDefaults, ~envUser), accounts: []}
|
|
257
282
|
}
|
|
258
283
|
}
|
|
259
|
-
if username == "" || password == "" {
|
|
284
|
+
if resolved.caller.username == "" || resolved.caller.password == "" {
|
|
260
285
|
throw(Failed("username and password are required."))
|
|
261
286
|
}
|
|
262
|
-
|
|
287
|
+
resolved
|
|
263
288
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -15,22 +15,18 @@ open JestGlobals
|
|
|
15
15
|
|
|
16
16
|
describe("Seed_Prompt.isAffirmative:", () => {
|
|
17
17
|
testSync("takes the three spellings of yes", () => {
|
|
18
|
-
["y", "yes", "1"]->Array.forEach(v =>
|
|
19
|
-
expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
|
|
20
|
-
)
|
|
18
|
+
["y", "yes", "1"]->Array.forEach(v => expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true))
|
|
21
19
|
})
|
|
22
20
|
|
|
23
21
|
// A prompt that prints `[y/N]` invites a capital as readily as a lowercase
|
|
24
22
|
// one, and an env var set by hand is written however the hand felt.
|
|
25
23
|
testSync("ignores case, so Y and YES answer as y does", () => {
|
|
26
|
-
["Y", "Yes", "YES"]->Array.forEach(v =>
|
|
27
|
-
expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
|
|
28
|
-
)
|
|
24
|
+
["Y", "Yes", "YES"]->Array.forEach(v => expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true))
|
|
29
25
|
})
|
|
30
26
|
|
|
31
27
|
testSync("ignores surrounding space", () => {
|
|
32
|
-
[" y", "y ", " yes "]->Array.forEach(
|
|
33
|
-
expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
|
|
28
|
+
[" y", "y ", " yes "]->Array.forEach(
|
|
29
|
+
v => expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true),
|
|
34
30
|
)
|
|
35
31
|
})
|
|
36
32
|
|
|
@@ -38,14 +34,14 @@ describe("Seed_Prompt.isAffirmative:", () => {
|
|
|
38
34
|
// reads it as "no". None may be mistaken for "nothing was said", which is what
|
|
39
35
|
// sent the old code to a prompt it could not open.
|
|
40
36
|
testSync("declines every spelling of no, capital N included", () => {
|
|
41
|
-
["n", "N", "no", "No", "NO"]->Array.forEach(
|
|
42
|
-
expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false)
|
|
37
|
+
["n", "N", "no", "No", "NO"]->Array.forEach(
|
|
38
|
+
v => expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false),
|
|
43
39
|
)
|
|
44
40
|
})
|
|
45
41
|
|
|
46
42
|
testSync("declines anything it does not recognise", () => {
|
|
47
|
-
["", " ", "0", "true", "yep", "y.", "yesplease", "affirmative"]->Array.forEach(
|
|
48
|
-
expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false)
|
|
43
|
+
["", " ", "0", "true", "yep", "y.", "yesplease", "affirmative"]->Array.forEach(
|
|
44
|
+
v => expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false),
|
|
49
45
|
)
|
|
50
46
|
})
|
|
51
47
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -10,13 +10,15 @@ type t = {mutable state: int}
|
|
|
10
10
|
|
|
11
11
|
let make = (~seed: int): t => {state: seed}
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
/** mulberry32 — small, fast, and stable across JS engines and Node versions. */
|
|
15
14
|
let float = (r: t): float => {
|
|
16
15
|
r.state = (r.state + 0x6d2b79f5)->Int.bitwiseOr(0)
|
|
17
16
|
let a = r.state
|
|
18
17
|
let b = imul(a->Int.bitwiseXor(a->Int.shiftRightUnsigned(15)), a->Int.bitwiseOr(1))
|
|
19
|
-
let c =
|
|
18
|
+
let c =
|
|
19
|
+
b->Int.bitwiseXor(
|
|
20
|
+
b + imul(b->Int.bitwiseXor(b->Int.shiftRightUnsigned(7)), b->Int.bitwiseOr(61)),
|
|
21
|
+
)
|
|
20
22
|
let bits = c->Int.bitwiseXor(c->Int.shiftRightUnsigned(14))
|
|
21
23
|
// `bits` is a signed 32-bit result; fold the sign bit back in to get the
|
|
22
24
|
// unsigned value a `>>> 0` would produce.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -16,36 +16,45 @@ describe("Seed_Random:", () => {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
describe("reproducibility — the reason this module exists:", () => {
|
|
19
|
-
testSync(
|
|
20
|
-
|
|
19
|
+
testSync(
|
|
20
|
+
"the same seed replays the same sequence",
|
|
21
|
+
() => expect(take(~seed=42, ~n=25))->toEqual(take(~seed=42, ~n=25)),
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
// Without this, a generator that returned a constant would satisfy the
|
|
24
25
|
// case above and nothing else in the suite would catch it.
|
|
25
|
-
testSync(
|
|
26
|
-
|
|
26
|
+
testSync(
|
|
27
|
+
"different seeds diverge",
|
|
28
|
+
() => expect(take(~seed=42, ~n=25) == take(~seed=43, ~n=25))->toBe(false),
|
|
27
29
|
)
|
|
28
30
|
|
|
29
|
-
testSync(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
testSync(
|
|
32
|
+
"a fresh generator does not continue the previous one",
|
|
33
|
+
() => {
|
|
34
|
+
let r = Seed_Random.make(~seed=7)
|
|
35
|
+
let first = Seed_Random.float(r)
|
|
36
|
+
let _ = Seed_Random.float(r)
|
|
37
|
+
expect(Seed_Random.float(Seed_Random.make(~seed=7)))->toEqual(first)
|
|
38
|
+
},
|
|
39
|
+
)
|
|
35
40
|
})
|
|
36
41
|
|
|
37
42
|
describe("float stays a unit interval:", () => {
|
|
38
43
|
// mulberry32's last step is a signed 32-bit value folded back to unsigned.
|
|
39
44
|
// Get that fold wrong and roughly half the draws come out negative, which
|
|
40
45
|
// every consumer below (int, pick, sampleWeighted) silently mis-scales.
|
|
41
|
-
testSync(
|
|
42
|
-
|
|
46
|
+
testSync(
|
|
47
|
+
"every draw is in [0, 1)",
|
|
48
|
+
() => expect(take(~seed=99, ~n=500)->Array.every(f => f >= 0.0 && f < 1.0))->toBe(true),
|
|
43
49
|
)
|
|
44
50
|
|
|
45
|
-
testSync(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
testSync(
|
|
52
|
+
"and the draws are not all the same value",
|
|
53
|
+
() => {
|
|
54
|
+
let draws = take(~seed=99, ~n=100)
|
|
55
|
+
expect(draws->Array.some(f => f != draws->Array.getUnsafe(0)))->toBe(true)
|
|
56
|
+
},
|
|
57
|
+
)
|
|
49
58
|
})
|
|
50
59
|
|
|
51
60
|
describe("int is inclusive on both ends, as documented:", () => {
|
|
@@ -54,40 +63,50 @@ describe("Seed_Random:", () => {
|
|
|
54
63
|
Array.make(~length=n, 0)->Array.map(_ => Seed_Random.int(r, ~min, ~max))
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
testSync(
|
|
58
|
-
|
|
66
|
+
testSync(
|
|
67
|
+
"never escapes the range",
|
|
68
|
+
() => expect(draws(~min=3, ~max=7, ~n=500)->Array.every(i => i >= 3 && i <= 7))->toBe(true),
|
|
59
69
|
)
|
|
60
70
|
|
|
61
71
|
// The documented contract is "inclusive on both ends"; an off-by-one in the
|
|
62
72
|
// `max - min + 1` scaling shows up as a bound that is never drawn.
|
|
63
|
-
testSync(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
testSync(
|
|
74
|
+
"reaches both bounds",
|
|
75
|
+
() => {
|
|
76
|
+
let d = draws(~min=3, ~max=7, ~n=500)
|
|
77
|
+
expect((d->Array.includes(3), d->Array.includes(7)))->toEqual((true, true))
|
|
78
|
+
},
|
|
79
|
+
)
|
|
67
80
|
|
|
68
|
-
testSync(
|
|
69
|
-
|
|
81
|
+
testSync(
|
|
82
|
+
"a single-value range is that value",
|
|
83
|
+
() => expect(draws(~min=4, ~max=4, ~n=10)->Array.every(i => i == 4))->toBe(true),
|
|
70
84
|
)
|
|
71
85
|
})
|
|
72
86
|
|
|
73
87
|
describe("pick:", () => {
|
|
74
|
-
testSync(
|
|
75
|
-
|
|
88
|
+
testSync(
|
|
89
|
+
"returns None for an empty array",
|
|
90
|
+
() => expect(Seed_Random.make(~seed=1)->Seed_Random.pick([]))->toEqual(None),
|
|
76
91
|
)
|
|
77
92
|
|
|
78
|
-
testSync(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
testSync(
|
|
94
|
+
"always lands on a member",
|
|
95
|
+
() => {
|
|
96
|
+
let r = Seed_Random.make(~seed=2)
|
|
97
|
+
let xs = ["a", "b", "c", "d"]
|
|
98
|
+
let hits =
|
|
99
|
+
Array.make(~length=200, "")->Array.map(_ => r->Seed_Random.pickOr(~fallback="MISS", xs))
|
|
100
|
+
expect(hits->Array.every(h => xs->Array.includes(h)))->toBe(true)
|
|
101
|
+
},
|
|
102
|
+
)
|
|
86
103
|
|
|
87
|
-
testSync(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
104
|
+
testSync(
|
|
105
|
+
"pickOr falls back only when there is nothing to pick",
|
|
106
|
+
() =>
|
|
107
|
+
expect(Seed_Random.make(~seed=3)->Seed_Random.pickOr(~fallback="fallback", []))->toBe(
|
|
108
|
+
"fallback",
|
|
109
|
+
),
|
|
91
110
|
)
|
|
92
111
|
})
|
|
93
112
|
|
|
@@ -96,65 +115,86 @@ describe("Seed_Random:", () => {
|
|
|
96
115
|
|
|
97
116
|
// The documented shuffle use: equal weights, count = length. If sampling
|
|
98
117
|
// ever replaced instead of removing, duplicates would appear here.
|
|
99
|
-
testSync(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
118
|
+
testSync(
|
|
119
|
+
"count = length yields a permutation, not a multiset",
|
|
120
|
+
() => {
|
|
121
|
+
let xs = ["a", "b", "c", "d", "e"]
|
|
122
|
+
let out = Seed_Random.make(~seed=11)->Seed_Random.sampleWeighted(equal(xs), ~count=5)
|
|
123
|
+
let sorted = out->Array.toSorted(String.compare)
|
|
124
|
+
expect((out->Array.length, sorted))->toEqual((5, ["a", "b", "c", "d", "e"]))
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
testSync(
|
|
129
|
+
"count < length returns exactly count distinct items",
|
|
130
|
+
() => {
|
|
131
|
+
let xs = ["a", "b", "c", "d", "e"]
|
|
132
|
+
let out = Seed_Random.make(~seed=12)->Seed_Random.sampleWeighted(equal(xs), ~count=3)
|
|
133
|
+
let distinct = Set.fromArray(out)->Set.size
|
|
134
|
+
expect((out->Array.length, distinct))->toEqual((3, 3))
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
testSync(
|
|
139
|
+
"count > length clamps to the pool",
|
|
140
|
+
() =>
|
|
141
|
+
expect(
|
|
142
|
+
Seed_Random.make(~seed=13)
|
|
143
|
+
->Seed_Random.sampleWeighted(equal(["a", "b"]), ~count=99)
|
|
144
|
+
->Array.length,
|
|
145
|
+
)->toBe(2),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
testSync(
|
|
149
|
+
"a zero-weight entry is never chosen while others remain",
|
|
150
|
+
() => {
|
|
151
|
+
let out =
|
|
152
|
+
Seed_Random.make(~seed=14)->Seed_Random.sampleWeighted(
|
|
153
|
+
[("never", 0.0), ("a", 1.0), ("b", 1.0)],
|
|
154
|
+
~count=2,
|
|
155
|
+
)
|
|
156
|
+
expect(out->Array.includes("never"))->toBe(false)
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
testSync(
|
|
161
|
+
"is reproducible for a given seed",
|
|
162
|
+
() => {
|
|
163
|
+
let run = () =>
|
|
164
|
+
Seed_Random.make(~seed=15)->Seed_Random.sampleWeighted(
|
|
165
|
+
equal(["a", "b", "c", "d", "e", "f"]),
|
|
166
|
+
~count=4,
|
|
167
|
+
)
|
|
168
|
+
expect(run())->toEqual(run())
|
|
169
|
+
},
|
|
170
|
+
)
|
|
138
171
|
})
|
|
139
172
|
|
|
140
173
|
describe("zipfWeights skews toward the head:", () => {
|
|
141
174
|
let weights = Seed_Random.zipfWeights(["a", "b", "c", "d", "e"], ~exponent=1.1)
|
|
142
175
|
|
|
143
|
-
testSync(
|
|
144
|
-
|
|
176
|
+
testSync(
|
|
177
|
+
"keeps every item, in order",
|
|
178
|
+
() => expect(weights->Array.map(((item, _)) => item))->toEqual(["a", "b", "c", "d", "e"]),
|
|
145
179
|
)
|
|
146
180
|
|
|
147
|
-
testSync(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
ws->Array.
|
|
151
|
-
|
|
152
|
-
|
|
181
|
+
testSync(
|
|
182
|
+
"weight decreases monotonically with rank",
|
|
183
|
+
() => {
|
|
184
|
+
let ws = weights->Array.map(((_, w)) => w)
|
|
185
|
+
let decreasing =
|
|
186
|
+
ws->Array.everyWithIndex((w, i) => i == 0 || w < ws->Array.getUnsafe(i - 1))
|
|
187
|
+
expect(decreasing)->toBe(true)
|
|
188
|
+
},
|
|
189
|
+
)
|
|
153
190
|
|
|
154
|
-
testSync(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
191
|
+
testSync(
|
|
192
|
+
"the leader is clear — first outweighs last several times over",
|
|
193
|
+
() => {
|
|
194
|
+
let first = weights->Array.get(0)->Option.mapOr(0.0, ((_, w)) => w)
|
|
195
|
+
let last = weights->Array.get(4)->Option.mapOr(0.0, ((_, w)) => w)
|
|
196
|
+
expect(first > last *. 3.0)->toBe(true)
|
|
197
|
+
},
|
|
198
|
+
)
|
|
159
199
|
})
|
|
160
200
|
})
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
open Seed_Types
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
let envOr = (key: string, fallback: string): string =>
|
|
7
6
|
NodeProcess.env->Dict.get(key)->Option.getOr(fallback)
|
|
8
7
|
|
|
@@ -58,14 +57,12 @@ let verifyViews = async (client: Seed_Client.t, ~views: array<view>): dict<int>
|
|
|
58
57
|
Console.log(` ${count->Int.toString->String.padStart(4, " ")} ${name}${note}`)
|
|
59
58
|
})
|
|
60
59
|
|
|
61
|
-
let empty =
|
|
62
|
-
|
|
63
|
-
->
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
)
|
|
60
|
+
let empty = views->Array.filterMap(v =>
|
|
61
|
+
switch v {
|
|
62
|
+
| Seeded(name) if counts->Dict.get(name)->Option.getOr(0) == 0 => Some(name)
|
|
63
|
+
| _ => None
|
|
64
|
+
}
|
|
65
|
+
)
|
|
69
66
|
if empty->Array.length > 0 {
|
|
70
67
|
throw(Failed(`these views are still empty after seeding: ${empty->Array.join(", ")}`))
|
|
71
68
|
}
|
|
@@ -138,9 +135,7 @@ let run = async (main: unit => promise<unit>): unit =>
|
|
|
138
135
|
Console.error("")
|
|
139
136
|
Console.error("Seeding aborted with an unexpected error:")
|
|
140
137
|
Console.error("")
|
|
141
|
-
Console.error(
|
|
142
|
-
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown"),
|
|
143
|
-
)
|
|
138
|
+
Console.error(exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown"))
|
|
144
139
|
NodeProcess.exit(1)
|
|
145
140
|
}
|
|
146
141
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -64,7 +64,9 @@ let uploadAsset = async (
|
|
|
64
64
|
// GraphQL string literals: JSON-quote the argument values (JSON string escaping is a
|
|
65
65
|
// superset-safe subset of GraphQL's for these ASCII-ish inputs).
|
|
66
66
|
let q = s => JSON.stringify(JSON.Encode.string(s))
|
|
67
|
-
let query = `mutation { r: Upload_Presign(store: ${q(store)}, fileName: ${q(
|
|
67
|
+
let query = `mutation { r: Upload_Presign(store: ${q(store)}, fileName: ${q(
|
|
68
|
+
fileName,
|
|
69
|
+
)}, contentType: ${q(contentType)}) { uploadUrl storageRef } }`
|
|
68
70
|
|
|
69
71
|
let presign = try Ok(await Seed_Client.gql(client, ~query, ~label="Upload_Presign")) catch {
|
|
70
72
|
| Seed_Types.Failed(m) => Error(m)
|
|
@@ -89,7 +91,7 @@ let uploadAsset = async (
|
|
|
89
91
|
| Ok(putRes) if putRes->responseOk => Ok(storageRef)
|
|
90
92
|
| Ok(putRes) =>
|
|
91
93
|
let detail = await putRes->responseText
|
|
92
|
-
Error(`upload PUT failed with HTTP ${
|
|
94
|
+
Error(`upload PUT failed with HTTP ${putRes->responseStatus->Int.toString}: ${detail}`)
|
|
93
95
|
}
|
|
94
96
|
| _ => Error("Upload_Presign response missing uploadUrl/storageRef")
|
|
95
97
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/ocaml/Seed_Users.ast
CHANGED
|
Binary file
|
package/lib/ocaml/Seed_Users.res
CHANGED
|
@@ -11,8 +11,21 @@
|
|
|
11
11
|
|
|
12
12
|
@module("yaml") external parseYaml: string => JSON.t = "parse"
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* One account: what to log in as, what to send with it, and — when the file
|
|
16
|
+
* records one — the id the platform stamps on rows this account writes.
|
|
17
|
+
*
|
|
18
|
+
* `userId` is optional because absence is normal on both platforms: a local file
|
|
19
|
+
* may omit it and let the auth adapter default it to the username, and a
|
|
20
|
+
* hand-maintained AWS file records subs the pool minted, so it can lag.
|
|
21
|
+
*/
|
|
22
|
+
type user = {
|
|
23
|
+
username: string,
|
|
24
|
+
password: string,
|
|
25
|
+
groups: array<string>,
|
|
26
|
+
userId: option<string>,
|
|
27
|
+
demoOwner: option<string>,
|
|
28
|
+
}
|
|
16
29
|
|
|
17
30
|
let asString = (json: JSON.t): option<string> =>
|
|
18
31
|
switch json {
|
|
@@ -32,7 +45,13 @@ let userOf = (json: JSON.t): option<user> =>
|
|
|
32
45
|
| Some(JSON.Array(items)) => items->Array.filterMap(asString)
|
|
33
46
|
| _ => []
|
|
34
47
|
}
|
|
35
|
-
Some({
|
|
48
|
+
Some({
|
|
49
|
+
username,
|
|
50
|
+
password,
|
|
51
|
+
groups,
|
|
52
|
+
userId: obj->Dict.get("userId")->Option.flatMap(asString),
|
|
53
|
+
demoOwner: obj->Dict.get("demoOwner")->Option.flatMap(asString),
|
|
54
|
+
})
|
|
36
55
|
| _ => None
|
|
37
56
|
}
|
|
38
57
|
| _ => None
|
|
@@ -75,6 +94,14 @@ let load = (~path: option<string>=?): option<(string, array<user>)> => {
|
|
|
75
94
|
users->Array.length == 0 ? None : Some((file, users))
|
|
76
95
|
}
|
|
77
96
|
|
|
97
|
+
/** The account that plays `name` in a demo: the one whose `demoOwner` says so,
|
|
98
|
+
else the one named `name` — which is how a file without the field reads. */
|
|
99
|
+
let playing = (users: array<user>, name: string): option<user> =>
|
|
100
|
+
switch users->Array.find(u => u.demoOwner == Some(name)) {
|
|
101
|
+
| Some(_) as declared => declared
|
|
102
|
+
| None => users->Array.find(u => u.username == name)
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
/** Menu label: the username, plus whatever groups the file records for it. */
|
|
79
106
|
let label = (u: user): string =>
|
|
80
107
|
u.groups->Array.length == 0 ? u.username : `${u.username} [${u.groups->Array.join(", ")}]`
|
|
Binary file
|
|
Binary file
|