@reventlessdev/reventless-gwt 1.0.0-alpha.75 → 1.0.0-alpha.77
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 +16 -0
- package/package.json +8 -8
- package/src/Behavior_GWT.res +125 -159
- package/src/Behavior_GWT.res.mjs +156 -59
- package/src/Bind.res.mjs +2 -2
- package/src/BuildClassifier.res +15 -5
- package/src/BuildClassifier.res.mjs +3 -3
- package/src/Cancellation.res +6 -0
- package/src/Cancellation.res.mjs +3 -2
- package/src/ChildProcess.res +19 -1
- package/src/ChildProcess.res.mjs +16 -1
- package/src/Cli.res +260 -26
- package/src/Cli.res.mjs +227 -29
- package/src/Collector.res +13 -2
- package/src/Collector.res.mjs +8 -5
- package/src/ComponentMeta.res +16 -43
- package/src/ComponentMeta.res.mjs +12 -41
- package/src/Discovery.res +19 -19
- package/src/Discovery.res.mjs +5 -9
- package/src/DomainGraph.res +0 -0
- package/src/DomainGraph.res.mjs +0 -0
- package/src/Filter.res +11 -2
- package/src/Filter.res.mjs +10 -3
- package/src/FormatterHuman.res +14 -43
- package/src/FormatterHuman.res.mjs +21 -28
- package/src/FormatterJson.res +12 -5
- package/src/FormatterJson.res.mjs +8 -6
- package/src/FormatterTap.res +13 -52
- package/src/FormatterTap.res.mjs +25 -43
- package/src/FormatterVsCode.res +4 -19
- package/src/FormatterVsCode.res.mjs +4 -23
- package/src/JestBind.res +1 -1
- package/src/JestBind.res.mjs +2 -2
- package/src/LocalHost.res +11 -27
- package/src/LocalHost.res.mjs +6 -34
- package/src/MismatchRender.res +72 -0
- package/src/MismatchRender.res.mjs +127 -0
- package/src/Outcome.res +0 -64
- package/src/Outcome.res.mjs +0 -47
- package/src/PlatformRunner.res +27 -6
- package/src/PlatformRunner.res.mjs +27 -6
- package/src/PlatformScan.res +14 -10
- package/src/PlatformScan.res.mjs +14 -13
- package/src/ProcessManager.res +51 -7
- package/src/ProcessManager.res.mjs +43 -4
- package/src/RunWorker.res +24 -0
- package/src/RunWorker.res.mjs +16 -0
- package/src/Watch.res +46 -30
- package/src/Watch.res.mjs +34 -32
- package/src/WatcherProbe.res +11 -3
- package/src/WatcherProbe.res.mjs +15 -2
- package/src/Worker.res +24 -0
- package/src/Worker.res.mjs +9 -0
- package/tests/BuildClassifierTest.res +39 -0
- package/tests/BuildClassifierTest.res.mjs +44 -0
- package/tests/CliRunEntryTest.res +57 -0
- package/tests/CliRunEntryTest.res.mjs +65 -0
- package/tests/CliWatchScopeTest.res +62 -0
- package/tests/CliWatchScopeTest.res.mjs +95 -0
- package/tests/ComponentMetaTest.res +15 -0
- package/tests/ComponentMetaTest.res.mjs +14 -0
- package/tests/DomainGraphTest.res +2 -2
- package/tests/FlowAggregateGwtTest.res +2 -0
- package/tests/FlowAggregateGwtTest.res.mjs +4 -0
- package/tests/FormatterGoldenTest.res +125 -0
- package/tests/FormatterGoldenTest.res.mjs +162 -0
- package/tests/MappingGwtTest.res +2 -0
- package/tests/MappingGwtTest.res.mjs +4 -0
- package/tests/WatchDebounceTest.res +35 -7
- package/tests/WatchDebounceTest.res.mjs +37 -6
package/src/JestBind.res
CHANGED
|
@@ -60,7 +60,7 @@ let testPromise = (
|
|
|
60
60
|
) =>
|
|
61
61
|
if Collector.isActive() {
|
|
62
62
|
let location = Collector.captureLocation(1)
|
|
63
|
-
Collector.push(~slice?, ~location?, name, body)
|
|
63
|
+
Collector.push(~slice?, ~location?, ~timeout?, name, body)
|
|
64
64
|
} else {
|
|
65
65
|
switch timeout {
|
|
66
66
|
| Some(t) =>
|
package/src/JestBind.res.mjs
CHANGED
|
@@ -35,7 +35,7 @@ function todo(label) {
|
|
|
35
35
|
function test(slice, name, body) {
|
|
36
36
|
if (Collector$ReventlessGwt.isActive()) {
|
|
37
37
|
let location = Collector$ReventlessGwt.captureLocation(1);
|
|
38
|
-
return Collector$ReventlessGwt.push(slice, location, name, () => Promise.resolve(body()));
|
|
38
|
+
return Collector$ReventlessGwt.push(slice, location, undefined, name, () => Promise.resolve(body()));
|
|
39
39
|
}
|
|
40
40
|
globalThis.test(name, () => assertOutcome(slice, body()));
|
|
41
41
|
}
|
|
@@ -50,7 +50,7 @@ function testPromise(slice, name, timeout, body) {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
let location = Collector$ReventlessGwt.captureLocation(1);
|
|
53
|
-
Collector$ReventlessGwt.push(slice, location, name, body);
|
|
53
|
+
Collector$ReventlessGwt.push(slice, location, timeout, name, body);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export {
|
package/src/LocalHost.res
CHANGED
|
@@ -55,23 +55,11 @@ type pluginExports = {"Make": platform => builtPlugin}
|
|
|
55
55
|
// plugin keys match the names the framework itself bakes into Plugin.res. ──
|
|
56
56
|
|
|
57
57
|
// "@scope/my-catalog" → "MyCatalog", "online-shop" → "OnlineShop".
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
base
|
|
65
|
-
->String.split("-")
|
|
66
|
-
->Array.flatMap(part => part->String.split("_"))
|
|
67
|
-
->Array.map(word =>
|
|
68
|
-
word === ""
|
|
69
|
-
? ""
|
|
70
|
-
: word->String.slice(~start=0, ~end=1)->String.toUpperCase ++
|
|
71
|
-
word->String.slice(~start=1, ~end=word->String.length)
|
|
72
|
-
)
|
|
73
|
-
->Array.join("")
|
|
74
|
-
}
|
|
58
|
+
// Single-sourced with the plugin generator (spec's `Config`) via
|
|
59
|
+
// `Reventless.PluginName` so the two can't drift — a drift silently breaks
|
|
60
|
+
// graph plugin keys (see the module for the full rationale). Re-exported here
|
|
61
|
+
// under the historical name the tests already pin.
|
|
62
|
+
let packageNameToPluginName = Reventless.PluginName.fromPackageName
|
|
75
63
|
|
|
76
64
|
let strField = (json, key) =>
|
|
77
65
|
json->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))->Option.flatMap(JSON.Decode.string)
|
|
@@ -82,19 +70,15 @@ let readJson = path =>
|
|
|
82
70
|
}
|
|
83
71
|
|
|
84
72
|
// plugin.json `name` → else PascalCase(package.json `name`) → else "Plugin".
|
|
73
|
+
// The precedence itself lives in `Reventless.PluginName.resolve`; this only
|
|
74
|
+
// reads the two raw fields with the local node bindings.
|
|
85
75
|
let derivePluginName = (~pluginSrcDir: string): string => {
|
|
86
76
|
let pluginJson = join(pluginSrcDir, "plugin.json")
|
|
87
|
-
let
|
|
77
|
+
let pluginJsonName =
|
|
88
78
|
existsSync(pluginJson) ? readJson(pluginJson)->Option.flatMap(j => strField(j, "name")) : None
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
let pkgJson = join(dirname(pluginSrcDir), "package.json")
|
|
93
|
-
readJson(pkgJson)
|
|
94
|
-
->Option.flatMap(j => strField(j, "name"))
|
|
95
|
-
->Option.map(packageNameToPluginName)
|
|
96
|
-
->Option.getOr("Plugin")
|
|
97
|
-
}
|
|
79
|
+
let packageJsonName =
|
|
80
|
+
readJson(join(dirname(pluginSrcDir), "package.json"))->Option.flatMap(j => strField(j, "name"))
|
|
81
|
+
Reventless.PluginName.resolve(~pluginJsonName, ~packageJsonName)
|
|
98
82
|
}
|
|
99
83
|
|
|
100
84
|
// Given workspace package dirs (e.g. from PackageScan), keep those that carry a
|
package/src/LocalHost.res.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
|
7
7
|
import * as Nodemodule from "node:module";
|
|
8
8
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
9
9
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
10
|
+
import * as PluginName$Reventless from "@reventlessdev/reventless-spec/src/components/PluginName.res.mjs";
|
|
10
11
|
|
|
11
12
|
let dynamicImport = ((u) => import(u));
|
|
12
13
|
|
|
@@ -19,34 +20,6 @@ function bustedUrl(absolutePath) {
|
|
|
19
20
|
return Nodeurl.pathToFileURL(absolutePath).href + "?t=" + counter.contents.toString();
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
function packageNameToPluginName(packageName) {
|
|
23
|
-
let parts = packageName.split("/");
|
|
24
|
-
let len = parts.length;
|
|
25
|
-
let base;
|
|
26
|
-
if (len >= 3) {
|
|
27
|
-
base = parts[parts.length - 1 | 0];
|
|
28
|
-
} else {
|
|
29
|
-
switch (len) {
|
|
30
|
-
case 0 :
|
|
31
|
-
base = parts[parts.length - 1 | 0];
|
|
32
|
-
break;
|
|
33
|
-
case 1 :
|
|
34
|
-
base = parts[0];
|
|
35
|
-
break;
|
|
36
|
-
case 2 :
|
|
37
|
-
base = parts[1];
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return base.split("-").flatMap(part => part.split("_")).map(word => {
|
|
42
|
-
if (word === "") {
|
|
43
|
-
return "";
|
|
44
|
-
} else {
|
|
45
|
-
return word.slice(0, 1).toUpperCase() + word.slice(1, word.length);
|
|
46
|
-
}
|
|
47
|
-
}).join("");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
23
|
function strField(json, key) {
|
|
51
24
|
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d[key]), Stdlib_JSON.Decode.string);
|
|
52
25
|
}
|
|
@@ -61,12 +34,9 @@ function readJson(path) {
|
|
|
61
34
|
|
|
62
35
|
function derivePluginName(pluginSrcDir) {
|
|
63
36
|
let pluginJson = Nodepath.join(pluginSrcDir, "plugin.json");
|
|
64
|
-
let
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
let pkgJson = Nodepath.join(Nodepath.dirname(pluginSrcDir), "package.json");
|
|
69
|
-
return Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(readJson(pkgJson), j => strField(j, "name")), packageNameToPluginName), "Plugin");
|
|
37
|
+
let pluginJsonName = Nodefs.existsSync(pluginJson) ? Stdlib_Option.flatMap(readJson(pluginJson), j => strField(j, "name")) : undefined;
|
|
38
|
+
let packageJsonName = Stdlib_Option.flatMap(readJson(Nodepath.join(Nodepath.dirname(pluginSrcDir), "package.json")), j => strField(j, "name"));
|
|
39
|
+
return PluginName$Reventless.resolve(pluginJsonName, packageJsonName);
|
|
70
40
|
}
|
|
71
41
|
|
|
72
42
|
function discover(packageDirs) {
|
|
@@ -111,6 +81,8 @@ async function loadGraph(platformModulePath, plugins) {
|
|
|
111
81
|
};
|
|
112
82
|
}
|
|
113
83
|
|
|
84
|
+
let packageNameToPluginName = PluginName$Reventless.fromPackageName;
|
|
85
|
+
|
|
114
86
|
export {
|
|
115
87
|
dynamicImport,
|
|
116
88
|
counter,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Single source for rendering an `Outcome.mismatch`'s expected/actual/extras
|
|
2
|
+
// into RenderRescript strings. The Human and TAP formatters previously each held
|
|
3
|
+
// a full 10-arm switch over the mismatch that computed *identical* rendered
|
|
4
|
+
// values (`RenderRescript.render*`, the `Error()`/`Ok()` wrapping, the Todo
|
|
5
|
+
// `(id, value)` pairs) and differed only in framing — aligned `label: value`
|
|
6
|
+
// lines vs YAML `key: "escaped"` lines. This normalizes the value-rendering once;
|
|
7
|
+
// each formatter interprets the `field` list with its own framing.
|
|
8
|
+
//
|
|
9
|
+
// NOT used by JUnit (renders via the older JSON-based `Outcome.format`) nor
|
|
10
|
+
// FormatterJson/VsCode (they consume the raw JSON values to build a structured
|
|
11
|
+
// `{type, payload, rendered}` + `fieldDiff` payload).
|
|
12
|
+
|
|
13
|
+
// One rendered piece of a mismatch. The constructor carries the *already
|
|
14
|
+
// rendered* string (unescaped — Human uses it raw, TAP yaml-escapes it), except
|
|
15
|
+
// the structural markers `ExpectedNoEvents` (Human prints a literal line; TAP
|
|
16
|
+
// omits it) and `Stack` (Human appends it raw; TAP labels it as a yaml field).
|
|
17
|
+
type field =
|
|
18
|
+
| Expected(string)
|
|
19
|
+
| Actual(string)
|
|
20
|
+
| Key(string)
|
|
21
|
+
| ExpectedNoEvents
|
|
22
|
+
| Error(string)
|
|
23
|
+
| Stack(string)
|
|
24
|
+
|
|
25
|
+
type normalized = {
|
|
26
|
+
kind: string,
|
|
27
|
+
fields: array<field>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let render = RenderRescript.render
|
|
31
|
+
let renderMany = RenderRescript.renderMany
|
|
32
|
+
let renderOption = RenderRescript.renderOption
|
|
33
|
+
|
|
34
|
+
let normalize = (m: Outcome.mismatch): normalized => {
|
|
35
|
+
let fields = switch m {
|
|
36
|
+
| EventsMismatch({expected, actual}) => [Expected(renderMany(expected)), Actual(renderMany(actual))]
|
|
37
|
+
| ErrorMismatch({expected, actual, actualEvents}) => [
|
|
38
|
+
Expected(`Error(${render(expected)})`),
|
|
39
|
+
Actual(
|
|
40
|
+
switch actual {
|
|
41
|
+
| Some(v) => `Error(${render(v)})`
|
|
42
|
+
| None => `Ok(${renderMany(actualEvents)})`
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
]
|
|
46
|
+
| StateMismatch({key, expected, actual}) => [
|
|
47
|
+
Key(key),
|
|
48
|
+
Expected(renderOption(expected)),
|
|
49
|
+
Actual(renderOption(actual)),
|
|
50
|
+
]
|
|
51
|
+
| NoEventExpected({actual}) => [ExpectedNoEvents, Actual(renderMany(actual))]
|
|
52
|
+
| TodoMismatch({expected, actual}) =>
|
|
53
|
+
let fmt = (arr: array<(string, JSON.t)>) =>
|
|
54
|
+
"[" ++ arr->Array.map(((id, v)) => `(${id}, ${render(v)})`)->Array.join(", ") ++ "]"
|
|
55
|
+
[Expected(fmt(expected)), Actual(fmt(actual))]
|
|
56
|
+
| AppendConditionMismatch({expected, actual}) => [
|
|
57
|
+
Expected(render(expected)),
|
|
58
|
+
Actual(render(actual)),
|
|
59
|
+
]
|
|
60
|
+
| TranslateError({expected, actual}) => [
|
|
61
|
+
Expected(expected),
|
|
62
|
+
Actual(actual->Option.getOr("(none)")),
|
|
63
|
+
]
|
|
64
|
+
| QueryRowsMismatch({expected, actual}) => [Expected(renderMany(expected)), Actual(renderMany(actual))]
|
|
65
|
+
| PublishedActionsMismatch({expected, actual}) => [
|
|
66
|
+
Expected(renderMany(expected)),
|
|
67
|
+
Actual(renderMany(actual)),
|
|
68
|
+
]
|
|
69
|
+
| Throw({error, stack}) => [Error(error), Stack(stack)]
|
|
70
|
+
}
|
|
71
|
+
{kind: Outcome.kindName(m), fields}
|
|
72
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
|
|
5
|
+
import * as RenderRescript$ReventlessGwt from "./RenderRescript.res.mjs";
|
|
6
|
+
|
|
7
|
+
function normalize(m) {
|
|
8
|
+
let fields;
|
|
9
|
+
switch (m.TAG) {
|
|
10
|
+
case "ErrorMismatch" :
|
|
11
|
+
let actual = m.actual;
|
|
12
|
+
fields = [
|
|
13
|
+
{
|
|
14
|
+
TAG: "Expected",
|
|
15
|
+
_0: `Error(` + RenderRescript$ReventlessGwt.render(undefined, m.expected) + `)`
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
TAG: "Actual",
|
|
19
|
+
_0: actual !== undefined ? `Error(` + RenderRescript$ReventlessGwt.render(undefined, actual) + `)` : `Ok(` + RenderRescript$ReventlessGwt.renderMany(m.actualEvents) + `)`
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
break;
|
|
23
|
+
case "StateMismatch" :
|
|
24
|
+
fields = [
|
|
25
|
+
{
|
|
26
|
+
TAG: "Key",
|
|
27
|
+
_0: m.key
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
TAG: "Expected",
|
|
31
|
+
_0: RenderRescript$ReventlessGwt.renderOption(m.expected)
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
TAG: "Actual",
|
|
35
|
+
_0: RenderRescript$ReventlessGwt.renderOption(m.actual)
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
break;
|
|
39
|
+
case "NoEventExpected" :
|
|
40
|
+
fields = [
|
|
41
|
+
"ExpectedNoEvents",
|
|
42
|
+
{
|
|
43
|
+
TAG: "Actual",
|
|
44
|
+
_0: RenderRescript$ReventlessGwt.renderMany(m.actual)
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
break;
|
|
48
|
+
case "TodoMismatch" :
|
|
49
|
+
let fmt = arr => "[" + arr.map(param => `(` + param[0] + `, ` + RenderRescript$ReventlessGwt.render(undefined, param[1]) + `)`).join(", ") + "]";
|
|
50
|
+
fields = [
|
|
51
|
+
{
|
|
52
|
+
TAG: "Expected",
|
|
53
|
+
_0: fmt(m.expected)
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
TAG: "Actual",
|
|
57
|
+
_0: fmt(m.actual)
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
break;
|
|
61
|
+
case "AppendConditionMismatch" :
|
|
62
|
+
fields = [
|
|
63
|
+
{
|
|
64
|
+
TAG: "Expected",
|
|
65
|
+
_0: RenderRescript$ReventlessGwt.render(undefined, m.expected)
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
TAG: "Actual",
|
|
69
|
+
_0: RenderRescript$ReventlessGwt.render(undefined, m.actual)
|
|
70
|
+
}
|
|
71
|
+
];
|
|
72
|
+
break;
|
|
73
|
+
case "TranslateError" :
|
|
74
|
+
fields = [
|
|
75
|
+
{
|
|
76
|
+
TAG: "Expected",
|
|
77
|
+
_0: m.expected
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
TAG: "Actual",
|
|
81
|
+
_0: Stdlib_Option.getOr(m.actual, "(none)")
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
break;
|
|
85
|
+
case "Throw" :
|
|
86
|
+
fields = [
|
|
87
|
+
{
|
|
88
|
+
TAG: "Error",
|
|
89
|
+
_0: m.error
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
TAG: "Stack",
|
|
93
|
+
_0: m.stack
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
fields = [
|
|
99
|
+
{
|
|
100
|
+
TAG: "Expected",
|
|
101
|
+
_0: RenderRescript$ReventlessGwt.renderMany(m.expected)
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
TAG: "Actual",
|
|
105
|
+
_0: RenderRescript$ReventlessGwt.renderMany(m.actual)
|
|
106
|
+
}
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
kind: Outcome$ReventlessGwt.kindName(m),
|
|
111
|
+
fields: fields
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let render = RenderRescript$ReventlessGwt.render;
|
|
116
|
+
|
|
117
|
+
let renderMany = RenderRescript$ReventlessGwt.renderMany;
|
|
118
|
+
|
|
119
|
+
let renderOption = RenderRescript$ReventlessGwt.renderOption;
|
|
120
|
+
|
|
121
|
+
export {
|
|
122
|
+
render,
|
|
123
|
+
renderMany,
|
|
124
|
+
renderOption,
|
|
125
|
+
normalize,
|
|
126
|
+
}
|
|
127
|
+
/* No side effect */
|
package/src/Outcome.res
CHANGED
|
@@ -95,67 +95,3 @@ let format = (m: mismatch) =>
|
|
|
95
95
|
)}`
|
|
96
96
|
| Throw({error, stack}) => `Throw: ${error}\n${stack}`
|
|
97
97
|
}
|
|
98
|
-
|
|
99
|
-
// Structured JSON payload per §3.3 of the analysis.
|
|
100
|
-
// Closed schema — consumers switch on the `kind` discriminator.
|
|
101
|
-
// Stage 7 enriches with `rendered` strings and `fieldDiff`.
|
|
102
|
-
let toJson = (m: mismatch): JSON.t => {
|
|
103
|
-
let obj = Dict.make()
|
|
104
|
-
obj->Dict.set("kind", JSON.Encode.string(kindName(m)))
|
|
105
|
-
let jsonArr = arr => JSON.Encode.array(arr)
|
|
106
|
-
let optJson = o => o->Option.getOr(JSON.Encode.null)
|
|
107
|
-
switch m {
|
|
108
|
-
| EventsMismatch({expected, actual}) => {
|
|
109
|
-
obj->Dict.set("expected", jsonArr(expected))
|
|
110
|
-
obj->Dict.set("actual", jsonArr(actual))
|
|
111
|
-
}
|
|
112
|
-
| ErrorMismatch({expected, actual, actualEvents}) => {
|
|
113
|
-
obj->Dict.set("expected", expected)
|
|
114
|
-
obj->Dict.set("actual", optJson(actual))
|
|
115
|
-
obj->Dict.set("actualEvents", jsonArr(actualEvents))
|
|
116
|
-
}
|
|
117
|
-
| StateMismatch({key, expected, actual}) => {
|
|
118
|
-
obj->Dict.set("key", JSON.Encode.string(key))
|
|
119
|
-
obj->Dict.set("expected", optJson(expected))
|
|
120
|
-
obj->Dict.set("actual", optJson(actual))
|
|
121
|
-
}
|
|
122
|
-
| NoEventExpected({actual}) => obj->Dict.set("actual", jsonArr(actual))
|
|
123
|
-
| TodoMismatch({expected, actual}) => {
|
|
124
|
-
let pairArr = pairs =>
|
|
125
|
-
pairs
|
|
126
|
-
->Array.map(((id, v)) => {
|
|
127
|
-
let d = Dict.make()
|
|
128
|
-
d->Dict.set("id", JSON.Encode.string(id))
|
|
129
|
-
d->Dict.set("value", v)
|
|
130
|
-
JSON.Encode.object(d)
|
|
131
|
-
})
|
|
132
|
-
->jsonArr
|
|
133
|
-
obj->Dict.set("expected", pairArr(expected))
|
|
134
|
-
obj->Dict.set("actual", pairArr(actual))
|
|
135
|
-
}
|
|
136
|
-
| AppendConditionMismatch({expected, actual}) => {
|
|
137
|
-
obj->Dict.set("expected", expected)
|
|
138
|
-
obj->Dict.set("actual", actual)
|
|
139
|
-
}
|
|
140
|
-
| TranslateError({expected, actual}) => {
|
|
141
|
-
obj->Dict.set("expected", JSON.Encode.string(expected))
|
|
142
|
-
obj->Dict.set(
|
|
143
|
-
"actual",
|
|
144
|
-
actual->Option.mapOr(JSON.Encode.null, JSON.Encode.string),
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
| QueryRowsMismatch({expected, actual}) => {
|
|
148
|
-
obj->Dict.set("expected", jsonArr(expected))
|
|
149
|
-
obj->Dict.set("actual", jsonArr(actual))
|
|
150
|
-
}
|
|
151
|
-
| PublishedActionsMismatch({expected, actual}) => {
|
|
152
|
-
obj->Dict.set("expected", jsonArr(expected))
|
|
153
|
-
obj->Dict.set("actual", jsonArr(actual))
|
|
154
|
-
}
|
|
155
|
-
| Throw({error, stack}) => {
|
|
156
|
-
obj->Dict.set("error", JSON.Encode.string(error))
|
|
157
|
-
obj->Dict.set("stack", JSON.Encode.string(stack))
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
JSON.Encode.object(obj)
|
|
161
|
-
}
|
package/src/Outcome.res.mjs
CHANGED
|
@@ -76,52 +76,6 @@ function format(m) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function toJson(m) {
|
|
80
|
-
let obj = {};
|
|
81
|
-
obj["kind"] = kindName(m);
|
|
82
|
-
switch (m.TAG) {
|
|
83
|
-
case "ErrorMismatch" :
|
|
84
|
-
obj["expected"] = m.expected;
|
|
85
|
-
obj["actual"] = Stdlib_Option.getOr(m.actual, null);
|
|
86
|
-
obj["actualEvents"] = m.actualEvents;
|
|
87
|
-
break;
|
|
88
|
-
case "StateMismatch" :
|
|
89
|
-
obj["key"] = m.key;
|
|
90
|
-
obj["expected"] = Stdlib_Option.getOr(m.expected, null);
|
|
91
|
-
obj["actual"] = Stdlib_Option.getOr(m.actual, null);
|
|
92
|
-
break;
|
|
93
|
-
case "NoEventExpected" :
|
|
94
|
-
obj["actual"] = m.actual;
|
|
95
|
-
break;
|
|
96
|
-
case "TodoMismatch" :
|
|
97
|
-
let pairArr = pairs => pairs.map(param => {
|
|
98
|
-
let d = {};
|
|
99
|
-
d["id"] = param[0];
|
|
100
|
-
d["value"] = param[1];
|
|
101
|
-
return d;
|
|
102
|
-
});
|
|
103
|
-
obj["expected"] = pairArr(m.expected);
|
|
104
|
-
obj["actual"] = pairArr(m.actual);
|
|
105
|
-
break;
|
|
106
|
-
case "TranslateError" :
|
|
107
|
-
obj["expected"] = m.expected;
|
|
108
|
-
obj["actual"] = Stdlib_Option.mapOr(m.actual, null, prim => prim);
|
|
109
|
-
break;
|
|
110
|
-
case "EventsMismatch" :
|
|
111
|
-
case "AppendConditionMismatch" :
|
|
112
|
-
case "QueryRowsMismatch" :
|
|
113
|
-
case "PublishedActionsMismatch" :
|
|
114
|
-
obj["expected"] = m.expected;
|
|
115
|
-
obj["actual"] = m.actual;
|
|
116
|
-
break;
|
|
117
|
-
case "Throw" :
|
|
118
|
-
obj["error"] = m.error;
|
|
119
|
-
obj["stack"] = m.stack;
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
return obj;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
79
|
let pass = {
|
|
126
80
|
TAG: "Ok",
|
|
127
81
|
_0: undefined
|
|
@@ -135,6 +89,5 @@ export {
|
|
|
135
89
|
stringifyOptJson,
|
|
136
90
|
stringifyJsonArray,
|
|
137
91
|
format,
|
|
138
|
-
toJson,
|
|
139
92
|
}
|
|
140
93
|
/* No side effect */
|
package/src/PlatformRunner.res
CHANGED
|
@@ -60,6 +60,9 @@ let openEphemeral = (): promise<(netServer, int)> =>
|
|
|
60
60
|
s->_listen(0, "127.0.0.1", () => resolve((s, (s->_address)["port"])))
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
+
let closeServer = (s: netServer): promise<unit> =>
|
|
64
|
+
Promise.make((resolve, _reject) => s->_close(() => resolve()))
|
|
65
|
+
|
|
63
66
|
// Hold all n open while collecting ports so they're guaranteed distinct, then
|
|
64
67
|
// release them just before the child binds.
|
|
65
68
|
let allocFreePorts = async (n: int): array<int> => {
|
|
@@ -70,7 +73,10 @@ let allocFreePorts = async (n: int): array<int> => {
|
|
|
70
73
|
servers->Array.push(s)
|
|
71
74
|
ports->Array.push(p)
|
|
72
75
|
}
|
|
73
|
-
|
|
76
|
+
// Await every close so the ports are actually released before the child binds
|
|
77
|
+
// them. Closing fire-and-forget returned the ports while the sockets were
|
|
78
|
+
// still bound, racing the child's bind into EADDRINUSE.
|
|
79
|
+
let _ = await Promise.all(servers->Array.map(closeServer))
|
|
74
80
|
ports
|
|
75
81
|
}
|
|
76
82
|
|
|
@@ -150,11 +156,26 @@ let run = async (
|
|
|
150
156
|
}
|
|
151
157
|
ChildProcess.onStdoutLine(proc, handle)
|
|
152
158
|
ChildProcess.onStderrLine(proc, handle)
|
|
153
|
-
ChildProcess.onClose(proc, code => callbacks.onStop(code))
|
|
154
159
|
|
|
155
|
-
// Keep the
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
// Keep the parent alive until either the operator cancels or the child exits.
|
|
161
|
+
// The previous shape awaited cancellation *forever* and always returned 0, so
|
|
162
|
+
// a crashed platform child left a zombie parent that reported success. Resolve
|
|
163
|
+
// on child close (or a spawn error) too, and surface the child's exit code.
|
|
164
|
+
let exitCode = ref(0)
|
|
165
|
+
await Promise.make((resolve, _reject) => {
|
|
166
|
+
Cancellation.onCancel(() => resolve())
|
|
167
|
+
ChildProcess.onError(proc, msg => {
|
|
168
|
+
callbacks.onLog("platform spawn error: " ++ msg)
|
|
169
|
+
callbacks.onStop(None)
|
|
170
|
+
exitCode := 1
|
|
171
|
+
resolve()
|
|
172
|
+
})
|
|
173
|
+
ChildProcess.onClose(proc, code => {
|
|
174
|
+
callbacks.onStop(code)
|
|
175
|
+
exitCode := code->Option.getOr(0)
|
|
176
|
+
resolve()
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
exitCode.contents
|
|
159
180
|
}
|
|
160
181
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Nodenet from "node:net";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
5
|
import * as Cancellation$ReventlessGwt from "./Cancellation.res.mjs";
|
|
5
6
|
import * as ChildProcess$ReventlessGwt from "./ChildProcess.res.mjs";
|
|
6
7
|
import * as PlatformScan$ReventlessGwt from "./PlatformScan.res.mjs";
|
|
@@ -44,6 +45,12 @@ function openEphemeral() {
|
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
function closeServer(s) {
|
|
49
|
+
return new Promise((resolve, _reject) => {
|
|
50
|
+
s.close(() => resolve());
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
47
54
|
async function allocFreePorts(n) {
|
|
48
55
|
let servers = [];
|
|
49
56
|
let ports = [];
|
|
@@ -52,9 +59,7 @@ async function allocFreePorts(n) {
|
|
|
52
59
|
servers.push(match[0]);
|
|
53
60
|
ports.push(match[1]);
|
|
54
61
|
}
|
|
55
|
-
servers.
|
|
56
|
-
s.close(() => {});
|
|
57
|
-
});
|
|
62
|
+
await Promise.all(servers.map(closeServer));
|
|
58
63
|
return ports;
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -136,9 +141,24 @@ async function run(roots, backend, fixedPortsOpt, callbacks) {
|
|
|
136
141
|
};
|
|
137
142
|
ChildProcess$ReventlessGwt.onStdoutLine(proc, handle);
|
|
138
143
|
ChildProcess$ReventlessGwt.onStderrLine(proc, handle);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
let exitCode = {
|
|
145
|
+
contents: 0
|
|
146
|
+
};
|
|
147
|
+
await new Promise((resolve, _reject) => {
|
|
148
|
+
Cancellation$ReventlessGwt.onCancel(() => resolve());
|
|
149
|
+
ChildProcess$ReventlessGwt.onError(proc, msg => {
|
|
150
|
+
callbacks.onLog("platform spawn error: " + msg);
|
|
151
|
+
callbacks.onStop(undefined);
|
|
152
|
+
exitCode.contents = 1;
|
|
153
|
+
resolve();
|
|
154
|
+
});
|
|
155
|
+
ChildProcess$ReventlessGwt.onClose(proc, code => {
|
|
156
|
+
callbacks.onStop(code);
|
|
157
|
+
exitCode.contents = Stdlib_Option.getOr(code, 0);
|
|
158
|
+
resolve();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
return exitCode.contents;
|
|
142
162
|
}
|
|
143
163
|
callbacks.onLog("no reventless-local platform package (src/Main.res.mjs + @reventlessdev/reventless-local) found under: " + roots.join(", "));
|
|
144
164
|
return 1;
|
|
@@ -148,6 +168,7 @@ export {
|
|
|
148
168
|
tapSentinel,
|
|
149
169
|
classifyLine,
|
|
150
170
|
openEphemeral,
|
|
171
|
+
closeServer,
|
|
151
172
|
allocFreePorts,
|
|
152
173
|
fixedPlatformPorts,
|
|
153
174
|
run,
|
package/src/PlatformScan.res
CHANGED
|
@@ -105,16 +105,20 @@ let inspectDir = (dir: string): option<platformPkg> => {
|
|
|
105
105
|
// declaration order preserved). Prunes node_modules/lib/.git like Discovery.
|
|
106
106
|
let rec walk = async (dir: string, acc: array<platformPkg>): array<platformPkg> => {
|
|
107
107
|
switch inspectDir(dir) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
108
|
+
// A platform package — record it and stop: descending into its own subtree
|
|
109
|
+
// (src/, generated code, nested workspace copies) can only waste I/O or turn
|
|
110
|
+
// up spurious inner matches. This is what the "don't descend" comment always
|
|
111
|
+
// intended; the recursion below used to run regardless.
|
|
112
|
+
| Some(pkg) => acc->Array.push(pkg)
|
|
113
|
+
| None =>
|
|
114
|
+
let entries = try await _readdir(dir, {withFileTypes: true}) catch {
|
|
115
|
+
| _ => []
|
|
116
|
+
}
|
|
117
|
+
for i in 0 to entries->Array.length - 1 {
|
|
118
|
+
let entry = entries->Array.getUnsafe(i)
|
|
119
|
+
if entry._isDirectory() && !Array.includes(ignoreNames, entry.name) {
|
|
120
|
+
let _ = await walk(join(dir, entry.name), acc)
|
|
121
|
+
}
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
124
|
acc
|
package/src/PlatformScan.res.mjs
CHANGED
|
@@ -83,19 +83,20 @@ async function walk(dir, acc) {
|
|
|
83
83
|
let pkg = inspectDir(dir);
|
|
84
84
|
if (pkg !== undefined) {
|
|
85
85
|
acc.push(pkg);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
86
|
+
} else {
|
|
87
|
+
let entries;
|
|
88
|
+
try {
|
|
89
|
+
entries = await Promises.readdir(dir, {
|
|
90
|
+
withFileTypes: true
|
|
91
|
+
});
|
|
92
|
+
} catch (exn) {
|
|
93
|
+
entries = [];
|
|
94
|
+
}
|
|
95
|
+
for (let i = 0, i_finish = entries.length; i < i_finish; ++i) {
|
|
96
|
+
let entry = entries[i];
|
|
97
|
+
if (entry.isDirectory() && !ignoreNames.includes(entry.name)) {
|
|
98
|
+
await walk(Nodepath.join(dir, entry.name), acc);
|
|
99
|
+
}
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
return acc;
|