@reventlessdev/reventless-gwt 1.0.0-alpha.100
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 +916 -0
- package/LICENSE +202 -0
- package/bin/reventless-dev.mjs +21 -0
- package/package.json +54 -0
- package/rescript.json +36 -0
- package/src/.gwtignore +10 -0
- package/src/Automation_GWT.res +207 -0
- package/src/Automation_GWT.res.mjs +160 -0
- package/src/Behavior_GWT.res +569 -0
- package/src/Behavior_GWT.res.mjs +523 -0
- package/src/Bind.res +29 -0
- package/src/Bind.res.mjs +22 -0
- package/src/BuildClassifier.res +147 -0
- package/src/BuildClassifier.res.mjs +136 -0
- package/src/Cancellation.res +37 -0
- package/src/Cancellation.res.mjs +46 -0
- package/src/ChildProcess.res +102 -0
- package/src/ChildProcess.res.mjs +107 -0
- package/src/Cli.res +905 -0
- package/src/Cli.res.mjs +952 -0
- package/src/Collector.res +212 -0
- package/src/Collector.res.mjs +218 -0
- package/src/ComponentMeta.res +77 -0
- package/src/ComponentMeta.res.mjs +91 -0
- package/src/ComponentScan.res +103 -0
- package/src/ComponentScan.res.mjs +105 -0
- package/src/Delegate_GWT.res +303 -0
- package/src/Delegate_GWT.res.mjs +372 -0
- package/src/Diff.res +91 -0
- package/src/Diff.res.mjs +98 -0
- package/src/Discovery.res +105 -0
- package/src/Discovery.res.mjs +99 -0
- package/src/DomainDeadCode.res +98 -0
- package/src/DomainDeadCode.res.mjs +72 -0
- package/src/DomainGraph.res +0 -0
- package/src/DomainGraph.res.mjs +0 -0
- package/src/EventMapping_GWT.res +29 -0
- package/src/EventMapping_GWT.res.mjs +44 -0
- package/src/Filter.res +45 -0
- package/src/Filter.res.mjs +47 -0
- package/src/Flow_GWT.res +664 -0
- package/src/Flow_GWT.res.mjs +604 -0
- package/src/FormatterHuman.res +109 -0
- package/src/FormatterHuman.res.mjs +115 -0
- package/src/FormatterJson.res +264 -0
- package/src/FormatterJson.res.mjs +322 -0
- package/src/FormatterJunit.res +93 -0
- package/src/FormatterJunit.res.mjs +90 -0
- package/src/FormatterTap.res +100 -0
- package/src/FormatterTap.res.mjs +131 -0
- package/src/FormatterVsCode.res +352 -0
- package/src/FormatterVsCode.res.mjs +546 -0
- package/src/Hint.res +119 -0
- package/src/Hint.res.mjs +124 -0
- package/src/InboundTranslation_GWT.res +83 -0
- package/src/InboundTranslation_GWT.res.mjs +94 -0
- package/src/JestBind.res +70 -0
- package/src/JestBind.res.mjs +63 -0
- package/src/Loader.res +24 -0
- package/src/Loader.res.mjs +23 -0
- package/src/LocalHost.res +119 -0
- package/src/LocalHost.res.mjs +99 -0
- package/src/Mapping_GWT.res +474 -0
- package/src/Mapping_GWT.res.mjs +322 -0
- package/src/MismatchRender.res +72 -0
- package/src/MismatchRender.res.mjs +127 -0
- package/src/MultiSourceProjection_GWT.res +316 -0
- package/src/MultiSourceProjection_GWT.res.mjs +354 -0
- package/src/OutboundTranslation_GWT.res +238 -0
- package/src/OutboundTranslation_GWT.res.mjs +186 -0
- package/src/Outcome.res +97 -0
- package/src/Outcome.res.mjs +93 -0
- package/src/PackageScan.res +80 -0
- package/src/PackageScan.res.mjs +81 -0
- package/src/PlatformRunner.res +181 -0
- package/src/PlatformRunner.res.mjs +176 -0
- package/src/PlatformScan.res +142 -0
- package/src/PlatformScan.res.mjs +132 -0
- package/src/ProcessManager.res +135 -0
- package/src/ProcessManager.res.mjs +119 -0
- package/src/Projection_GWT.res +312 -0
- package/src/Projection_GWT.res.mjs +332 -0
- package/src/Query_GWT.res +379 -0
- package/src/Query_GWT.res.mjs +367 -0
- package/src/RenderRescript.res +125 -0
- package/src/RenderRescript.res.mjs +141 -0
- package/src/RunWorker.res +24 -0
- package/src/RunWorker.res.mjs +16 -0
- package/src/RunnerTypes.res +68 -0
- package/src/RunnerTypes.res.mjs +60 -0
- package/src/SideEffect_GWT.res +157 -0
- package/src/SideEffect_GWT.res.mjs +113 -0
- package/src/StateChange/ExternalAddCategorySlice.res +25 -0
- package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
- package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
- package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
- package/src/StateChange/WithFixtures.res +22 -0
- package/src/StateChange/WithFixtures.res.mjs +31 -0
- package/src/StateChange/WithFixtures_Behavior.res +17 -0
- package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
- package/src/StateChange/WithManualOpen.res +23 -0
- package/src/StateChange/WithManualOpen.res.mjs +31 -0
- package/src/StateChange/WithManualOpen_Behavior.res +17 -0
- package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
- package/src/StubRuntime.res +54 -0
- package/src/StubRuntime.res.mjs +55 -0
- package/src/TestFixtures.res +16 -0
- package/src/TestFixtures.res.mjs +34 -0
- package/src/Watch.res +108 -0
- package/src/Watch.res.mjs +85 -0
- package/src/WatcherProbe.res +56 -0
- package/src/WatcherProbe.res.mjs +59 -0
- package/src/Worker.res +24 -0
- package/src/Worker.res.mjs +9 -0
- package/tests/AutomationGwtTest.res +71 -0
- package/tests/AutomationGwtTest.res.mjs +189 -0
- package/tests/BuildClassifierTest.res +133 -0
- package/tests/BuildClassifierTest.res.mjs +176 -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 +67 -0
- package/tests/ComponentMetaTest.res.mjs +71 -0
- package/tests/DelegateGwtTest.res +487 -0
- package/tests/DelegateGwtTest.res.mjs +891 -0
- package/tests/DiscoveryTest.res +42 -0
- package/tests/DiscoveryTest.res.mjs +43 -0
- package/tests/DomainDeadCodeTest.res +119 -0
- package/tests/DomainDeadCodeTest.res.mjs +158 -0
- package/tests/DomainGraphTest.res +448 -0
- package/tests/DomainGraphTest.res.mjs +505 -0
- package/tests/FlowAggregateGwtTest.res +171 -0
- package/tests/FlowAggregateGwtTest.res.mjs +273 -0
- package/tests/FlowCrossPluginGwtTest.res +340 -0
- package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
- package/tests/FlowGwtTest.res +222 -0
- package/tests/FlowGwtTest.res.mjs +433 -0
- package/tests/FormatterGoldenTest.res +125 -0
- package/tests/FormatterGoldenTest.res.mjs +162 -0
- package/tests/FormatterVsCodeMessageTest.res +41 -0
- package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
- package/tests/InboundTranslationGwtTest.res +34 -0
- package/tests/InboundTranslationGwtTest.res.mjs +99 -0
- package/tests/LocalHostIntegration.res +70 -0
- package/tests/LocalHostIntegration.res.mjs +64 -0
- package/tests/LocalHostTest.res +71 -0
- package/tests/LocalHostTest.res.mjs +63 -0
- package/tests/MappingGwtTest.res +350 -0
- package/tests/MappingGwtTest.res.mjs +596 -0
- package/tests/OutboundTranslationGwtTest.res +59 -0
- package/tests/OutboundTranslationGwtTest.res.mjs +139 -0
- package/tests/PlatformRunnerTest.res +66 -0
- package/tests/PlatformRunnerTest.res.mjs +85 -0
- package/tests/PlatformScanTest.res +61 -0
- package/tests/PlatformScanTest.res.mjs +56 -0
- package/tests/QueryGwtTest.res +161 -0
- package/tests/QueryGwtTest.res.mjs +314 -0
- package/tests/QueryResolverGwtTest.res +71 -0
- package/tests/QueryResolverGwtTest.res.mjs +136 -0
- package/tests/RunnerUnitTest.res +219 -0
- package/tests/RunnerUnitTest.res.mjs +244 -0
- package/tests/SideEffect/FakeOrderNotification.res +23 -0
- package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
- package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
- package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
- package/tests/SideEffect/MockEmail.res +18 -0
- package/tests/SideEffect/MockEmail.res.mjs +47 -0
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
- package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
- package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
- package/tests/StateChange/WithFixtures_GWT.res +23 -0
- package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
- package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
- package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
- package/tests/StateChange/WithManualOpen_GWT.res +17 -0
- package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
- package/tests/StateChangeSliceGwtTest.res +160 -0
- package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
- package/tests/StateViewSliceGwtTest.res +64 -0
- package/tests/StateViewSliceGwtTest.res.mjs +161 -0
- package/tests/WatchDebounceTest.res +106 -0
- package/tests/WatchDebounceTest.res.mjs +128 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
open ReventlessCore
|
|
2
|
+
|
|
3
|
+
// Minimal inline spec for OutboundTranslationSlice. `translate` is async and
|
|
4
|
+
// supplied at test-time via `whenTranslateMocked` so the external service is
|
|
5
|
+
// never hit. The spec's own `translate` is not part of the GWT — it's
|
|
6
|
+
// exercised only through the runtime tests.
|
|
7
|
+
module type SliceSpec = {
|
|
8
|
+
let name: string
|
|
9
|
+
|
|
10
|
+
@schema
|
|
11
|
+
type consumedEvent
|
|
12
|
+
|
|
13
|
+
@schema
|
|
14
|
+
type outboundItem
|
|
15
|
+
|
|
16
|
+
@schema
|
|
17
|
+
type inboundCommand
|
|
18
|
+
|
|
19
|
+
let collect: consumedEvent => array<(string, outboundItem)>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Status enum reflecting how the slice's runtime labels a TODO after a
|
|
23
|
+
// translate call. A successful translate (Ok) marks the item #Completed;
|
|
24
|
+
// a failure (Error) leaves it #Pending for retry up to `maxRetries`.
|
|
25
|
+
type todoStatus = [#Completed | #Pending]
|
|
26
|
+
|
|
27
|
+
module type T = {
|
|
28
|
+
module Spec: SliceSpec
|
|
29
|
+
|
|
30
|
+
let describe: (string, unit => unit) => unit
|
|
31
|
+
let test: (string, ~timeout: int=?, unit => promise<Outcome.outcome>) => unit
|
|
32
|
+
// Sync companion for the `collect` leg, whose combinators return
|
|
33
|
+
// `Outcome.outcome` directly (no Promise). The async `test` works for
|
|
34
|
+
// collect bodies too, but only after a manual `Promise.resolve` wrapper.
|
|
35
|
+
let testSync: (string, unit => Outcome.outcome) => unit
|
|
36
|
+
|
|
37
|
+
type translateResult =
|
|
38
|
+
result<option<(string, Spec.inboundCommand)>, string>
|
|
39
|
+
|
|
40
|
+
// The state the pipeline carries after a translate attempt. `retries` counts
|
|
41
|
+
// the number of failed re-attempts (0 for a single `whenTranslateMocked`).
|
|
42
|
+
type attempt = {
|
|
43
|
+
id: string,
|
|
44
|
+
item: Spec.outboundItem,
|
|
45
|
+
result: translateResult,
|
|
46
|
+
retries: int,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Unit — collect
|
|
50
|
+
let givenEvent: Spec.consumedEvent => Spec.consumedEvent
|
|
51
|
+
let whenCollect: Spec.consumedEvent => array<(string, Spec.outboundItem)>
|
|
52
|
+
let thenTodos: (
|
|
53
|
+
array<(string, Spec.outboundItem)>,
|
|
54
|
+
array<(string, Spec.outboundItem)>,
|
|
55
|
+
) => Outcome.outcome
|
|
56
|
+
|
|
57
|
+
// Unit — translate
|
|
58
|
+
let givenTodo: (string, Spec.outboundItem) => (string, Spec.outboundItem)
|
|
59
|
+
let whenTranslateMocked: (
|
|
60
|
+
(string, Spec.outboundItem),
|
|
61
|
+
(string, Spec.outboundItem) => promise<translateResult>,
|
|
62
|
+
) => promise<attempt>
|
|
63
|
+
// Re-invokes the mock on each `Error` up to `maxRetries` times (or until it
|
|
64
|
+
// returns `Ok`), tracking how many retries were spent — the real counter
|
|
65
|
+
// `thenRetryRecorded` asserts against.
|
|
66
|
+
let whenTranslateRetrying: (
|
|
67
|
+
(string, Spec.outboundItem),
|
|
68
|
+
~maxRetries: int,
|
|
69
|
+
(string, Spec.outboundItem) => promise<translateResult>,
|
|
70
|
+
) => promise<attempt>
|
|
71
|
+
let thenCommand: (
|
|
72
|
+
promise<attempt>,
|
|
73
|
+
string,
|
|
74
|
+
Spec.inboundCommand,
|
|
75
|
+
) => promise<Outcome.outcome>
|
|
76
|
+
let thenNoCommand: promise<attempt> => promise<Outcome.outcome>
|
|
77
|
+
let thenRetryRecorded: (promise<attempt>, int) => promise<Outcome.outcome>
|
|
78
|
+
let thenTodoStatus: (promise<attempt>, string, todoStatus) => promise<Outcome.outcome>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
|
|
82
|
+
module Spec = Spec
|
|
83
|
+
|
|
84
|
+
S.enableJson()
|
|
85
|
+
|
|
86
|
+
let describe = JestBind.describe
|
|
87
|
+
let test = (name, ~timeout=?, body) =>
|
|
88
|
+
JestBind.testPromise(~slice=Spec.name, name, ~timeout?, body)
|
|
89
|
+
let testSync = (name, body) => JestBind.test(~slice=Spec.name, name, body)
|
|
90
|
+
|
|
91
|
+
type translateResult =
|
|
92
|
+
result<option<(string, Spec.inboundCommand)>, string>
|
|
93
|
+
|
|
94
|
+
type attempt = {
|
|
95
|
+
id: string,
|
|
96
|
+
item: Spec.outboundItem,
|
|
97
|
+
result: translateResult,
|
|
98
|
+
retries: int,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let encItem = (i: Spec.outboundItem) => i->Message.encode(Spec.outboundItemSchema)
|
|
102
|
+
let encItems = (arr: array<(string, Spec.outboundItem)>) =>
|
|
103
|
+
arr->Array.map(((id, i)) => (id, encItem(i)))
|
|
104
|
+
let encInbound = (c: Spec.inboundCommand) =>
|
|
105
|
+
c->Message.encode(Spec.inboundCommandSchema)
|
|
106
|
+
|
|
107
|
+
// Unit: collect
|
|
108
|
+
let givenEvent = e => e
|
|
109
|
+
let whenCollect = e => e->Spec.collect
|
|
110
|
+
let thenTodos = (actual, expected) =>
|
|
111
|
+
if actual == expected {
|
|
112
|
+
Outcome.pass
|
|
113
|
+
} else {
|
|
114
|
+
Outcome.fail(
|
|
115
|
+
TodoMismatch({
|
|
116
|
+
expected: encItems(expected),
|
|
117
|
+
actual: encItems(actual),
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Unit: translate
|
|
123
|
+
let givenTodo = (id, item) => (id, item)
|
|
124
|
+
let whenTranslateMocked = async ((id, item), mock) => {
|
|
125
|
+
let result = await mock(id, item)
|
|
126
|
+
{id, item, result, retries: 0}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let whenTranslateRetrying = async ((id, item), ~maxRetries, mock) => {
|
|
130
|
+
let result = ref(await mock(id, item))
|
|
131
|
+
let retries = ref(0)
|
|
132
|
+
let failed = () =>
|
|
133
|
+
switch result.contents {
|
|
134
|
+
| Error(_) => true
|
|
135
|
+
| Ok(_) => false
|
|
136
|
+
}
|
|
137
|
+
while failed() && retries.contents < maxRetries {
|
|
138
|
+
retries := retries.contents + 1
|
|
139
|
+
result := await mock(id, item)
|
|
140
|
+
}
|
|
141
|
+
{id, item, result: result.contents, retries: retries.contents}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let commandPairJson = (id, cmd) => {
|
|
145
|
+
let d = Dict.make()
|
|
146
|
+
d->Dict.set("id", JSON.Encode.string(id))
|
|
147
|
+
d->Dict.set("command", encInbound(cmd))
|
|
148
|
+
JSON.Encode.object(d)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let thenCommand = async (pending, expectedId, expectedCmd) => {
|
|
152
|
+
let {result, _} = await pending
|
|
153
|
+
switch result {
|
|
154
|
+
| Ok(Some((id, cmd))) if id == expectedId && cmd == expectedCmd => Outcome.pass
|
|
155
|
+
| Ok(Some((id, cmd))) =>
|
|
156
|
+
Outcome.fail(
|
|
157
|
+
EventsMismatch({
|
|
158
|
+
expected: [commandPairJson(expectedId, expectedCmd)],
|
|
159
|
+
actual: [commandPairJson(id, cmd)],
|
|
160
|
+
}),
|
|
161
|
+
)
|
|
162
|
+
| Ok(None) =>
|
|
163
|
+
Outcome.fail(
|
|
164
|
+
EventsMismatch({
|
|
165
|
+
expected: [commandPairJson(expectedId, expectedCmd)],
|
|
166
|
+
actual: [],
|
|
167
|
+
}),
|
|
168
|
+
)
|
|
169
|
+
| Error(msg) =>
|
|
170
|
+
Outcome.fail(
|
|
171
|
+
TranslateError({expected: "(command)", actual: Some(msg)}),
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let thenNoCommand = async pending => {
|
|
177
|
+
let {result, _} = await pending
|
|
178
|
+
switch result {
|
|
179
|
+
| Ok(None) => Outcome.pass
|
|
180
|
+
| Ok(Some((id, cmd))) =>
|
|
181
|
+
Outcome.fail(NoEventExpected({actual: [commandPairJson(id, cmd)]}))
|
|
182
|
+
| Error(msg) =>
|
|
183
|
+
Outcome.fail(
|
|
184
|
+
TranslateError({expected: "(no command)", actual: Some(msg)}),
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// `thenRetryRecorded(n)` — asserts the harness spent exactly `n` retries.
|
|
190
|
+
// Pair with `whenTranslateRetrying(~maxRetries)`, which re-invokes the mock on
|
|
191
|
+
// each failure and tracks the count; a single `whenTranslateMocked` records 0.
|
|
192
|
+
let thenRetryRecorded = async (pending, expectedRetries) => {
|
|
193
|
+
let {retries, _} = await pending
|
|
194
|
+
if retries == expectedRetries {
|
|
195
|
+
Outcome.pass
|
|
196
|
+
} else {
|
|
197
|
+
Outcome.fail(
|
|
198
|
+
StateMismatch({
|
|
199
|
+
key: "retries",
|
|
200
|
+
expected: Some(JSON.Encode.int(expectedRetries)),
|
|
201
|
+
actual: Some(JSON.Encode.int(retries)),
|
|
202
|
+
}),
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let statusToString = (s: todoStatus) =>
|
|
208
|
+
switch s {
|
|
209
|
+
| #Completed => "#Completed"
|
|
210
|
+
| #Pending => "#Pending"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let thenTodoStatus = async (pending, expectedId, expectedStatus) => {
|
|
214
|
+
let {id, result, _} = await pending
|
|
215
|
+
let actualStatus: todoStatus =
|
|
216
|
+
switch result {
|
|
217
|
+
| Ok(_) => #Completed
|
|
218
|
+
| Error(_) => #Pending
|
|
219
|
+
}
|
|
220
|
+
if id == expectedId && actualStatus == expectedStatus {
|
|
221
|
+
Outcome.pass
|
|
222
|
+
} else {
|
|
223
|
+
let encOne = (rid, rstatus) => {
|
|
224
|
+
let d = Dict.make()
|
|
225
|
+
d->Dict.set("id", JSON.Encode.string(rid))
|
|
226
|
+
d->Dict.set("status", JSON.Encode.string(statusToString(rstatus)))
|
|
227
|
+
JSON.Encode.object(d)
|
|
228
|
+
}
|
|
229
|
+
Outcome.fail(
|
|
230
|
+
StateMismatch({
|
|
231
|
+
key: id,
|
|
232
|
+
expected: Some(encOne(expectedId, expectedStatus)),
|
|
233
|
+
actual: Some(encOne(id, actualStatus)),
|
|
234
|
+
}),
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
5
|
+
import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
|
|
6
|
+
import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
|
|
7
|
+
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
8
|
+
|
|
9
|
+
function Make(Spec) {
|
|
10
|
+
S.enableJson();
|
|
11
|
+
let test = (name, timeout, body) => JestBind$ReventlessGwt.testPromise(Spec.name, name, timeout, body);
|
|
12
|
+
let testSync = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
|
|
13
|
+
let encItems = arr => arr.map(param => [
|
|
14
|
+
param[0],
|
|
15
|
+
Message$ReventlessCore.encode(param[1], Spec.outboundItemSchema)
|
|
16
|
+
]);
|
|
17
|
+
let givenEvent = e => e;
|
|
18
|
+
let whenCollect = e => Spec.collect(e);
|
|
19
|
+
let thenTodos = (actual, expected) => {
|
|
20
|
+
if (Primitive_object.equal(actual, expected)) {
|
|
21
|
+
return Outcome$ReventlessGwt.pass;
|
|
22
|
+
} else {
|
|
23
|
+
return Outcome$ReventlessGwt.fail({
|
|
24
|
+
TAG: "TodoMismatch",
|
|
25
|
+
expected: encItems(expected),
|
|
26
|
+
actual: encItems(actual)
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
let givenTodo = (id, item) => [
|
|
31
|
+
id,
|
|
32
|
+
item
|
|
33
|
+
];
|
|
34
|
+
let whenTranslateMocked = async (param, mock) => {
|
|
35
|
+
let item = param[1];
|
|
36
|
+
let id = param[0];
|
|
37
|
+
let result = await mock(id, item);
|
|
38
|
+
return {
|
|
39
|
+
id: id,
|
|
40
|
+
item: item,
|
|
41
|
+
result: result,
|
|
42
|
+
retries: 0
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
let whenTranslateRetrying = async (param, maxRetries, mock) => {
|
|
46
|
+
let item = param[1];
|
|
47
|
+
let id = param[0];
|
|
48
|
+
let result = {
|
|
49
|
+
contents: await mock(id, item)
|
|
50
|
+
};
|
|
51
|
+
let retries = 0;
|
|
52
|
+
let failed = () => {
|
|
53
|
+
let match = result.contents;
|
|
54
|
+
return match.TAG !== "Ok";
|
|
55
|
+
};
|
|
56
|
+
while (failed() && retries < maxRetries) {
|
|
57
|
+
retries = retries + 1 | 0;
|
|
58
|
+
result.contents = await mock(id, item);
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
id: id,
|
|
62
|
+
item: item,
|
|
63
|
+
result: result.contents,
|
|
64
|
+
retries: retries
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
let commandPairJson = (id, cmd) => {
|
|
68
|
+
let d = {};
|
|
69
|
+
d["id"] = id;
|
|
70
|
+
d["command"] = Message$ReventlessCore.encode(cmd, Spec.inboundCommandSchema);
|
|
71
|
+
return d;
|
|
72
|
+
};
|
|
73
|
+
let thenCommand = async (pending, expectedId, expectedCmd) => {
|
|
74
|
+
let match = await pending;
|
|
75
|
+
let result = match.result;
|
|
76
|
+
if (result.TAG !== "Ok") {
|
|
77
|
+
return Outcome$ReventlessGwt.fail({
|
|
78
|
+
TAG: "TranslateError",
|
|
79
|
+
expected: "(command)",
|
|
80
|
+
actual: result._0
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
let match$1 = result._0;
|
|
84
|
+
if (match$1 === undefined) {
|
|
85
|
+
return Outcome$ReventlessGwt.fail({
|
|
86
|
+
TAG: "EventsMismatch",
|
|
87
|
+
expected: [commandPairJson(expectedId, expectedCmd)],
|
|
88
|
+
actual: []
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
let cmd = match$1[1];
|
|
92
|
+
let id = match$1[0];
|
|
93
|
+
if (id === expectedId && Primitive_object.equal(cmd, expectedCmd)) {
|
|
94
|
+
return Outcome$ReventlessGwt.pass;
|
|
95
|
+
} else {
|
|
96
|
+
return Outcome$ReventlessGwt.fail({
|
|
97
|
+
TAG: "EventsMismatch",
|
|
98
|
+
expected: [commandPairJson(expectedId, expectedCmd)],
|
|
99
|
+
actual: [commandPairJson(id, cmd)]
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
let thenNoCommand = async pending => {
|
|
104
|
+
let match = await pending;
|
|
105
|
+
let result = match.result;
|
|
106
|
+
if (result.TAG !== "Ok") {
|
|
107
|
+
return Outcome$ReventlessGwt.fail({
|
|
108
|
+
TAG: "TranslateError",
|
|
109
|
+
expected: "(no command)",
|
|
110
|
+
actual: result._0
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
let match$1 = result._0;
|
|
114
|
+
if (match$1 !== undefined) {
|
|
115
|
+
return Outcome$ReventlessGwt.fail({
|
|
116
|
+
TAG: "NoEventExpected",
|
|
117
|
+
actual: [commandPairJson(match$1[0], match$1[1])]
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
return Outcome$ReventlessGwt.pass;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
let thenRetryRecorded = async (pending, expectedRetries) => {
|
|
124
|
+
let match = await pending;
|
|
125
|
+
let retries = match.retries;
|
|
126
|
+
if (retries === expectedRetries) {
|
|
127
|
+
return Outcome$ReventlessGwt.pass;
|
|
128
|
+
} else {
|
|
129
|
+
return Outcome$ReventlessGwt.fail({
|
|
130
|
+
TAG: "StateMismatch",
|
|
131
|
+
key: "retries",
|
|
132
|
+
expected: expectedRetries,
|
|
133
|
+
actual: retries
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
let statusToString = s => {
|
|
138
|
+
if (s === "Pending") {
|
|
139
|
+
return "#Pending";
|
|
140
|
+
} else {
|
|
141
|
+
return "#Completed";
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
let thenTodoStatus = async (pending, expectedId, expectedStatus) => {
|
|
145
|
+
let match = await pending;
|
|
146
|
+
let id = match.id;
|
|
147
|
+
let actualStatus;
|
|
148
|
+
actualStatus = match.result.TAG === "Ok" ? "Completed" : "Pending";
|
|
149
|
+
if (id === expectedId && actualStatus === expectedStatus) {
|
|
150
|
+
return Outcome$ReventlessGwt.pass;
|
|
151
|
+
}
|
|
152
|
+
let encOne = (rid, rstatus) => {
|
|
153
|
+
let d = {};
|
|
154
|
+
d["id"] = rid;
|
|
155
|
+
d["status"] = statusToString(rstatus);
|
|
156
|
+
return d;
|
|
157
|
+
};
|
|
158
|
+
return Outcome$ReventlessGwt.fail({
|
|
159
|
+
TAG: "StateMismatch",
|
|
160
|
+
key: id,
|
|
161
|
+
expected: encOne(expectedId, expectedStatus),
|
|
162
|
+
actual: encOne(id, actualStatus)
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
Spec: Spec,
|
|
167
|
+
describe: JestBind$ReventlessGwt.describe,
|
|
168
|
+
test: test,
|
|
169
|
+
testSync: testSync,
|
|
170
|
+
givenEvent: givenEvent,
|
|
171
|
+
whenCollect: whenCollect,
|
|
172
|
+
thenTodos: thenTodos,
|
|
173
|
+
givenTodo: givenTodo,
|
|
174
|
+
whenTranslateMocked: whenTranslateMocked,
|
|
175
|
+
whenTranslateRetrying: whenTranslateRetrying,
|
|
176
|
+
thenCommand: thenCommand,
|
|
177
|
+
thenNoCommand: thenNoCommand,
|
|
178
|
+
thenRetryRecorded: thenRetryRecorded,
|
|
179
|
+
thenTodoStatus: thenTodoStatus
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export {
|
|
184
|
+
Make,
|
|
185
|
+
}
|
|
186
|
+
/* S Not a pure module */
|
package/src/Outcome.res
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// The `Outcome` algebra — Stage 2 of the reventless-gwt plan.
|
|
2
|
+
//
|
|
3
|
+
// Every `then*` combinator in every GWT DSL returns `Outcome.outcome`
|
|
4
|
+
// (`result<unit, mismatch>`). The runner, the human formatter, the AI loop,
|
|
5
|
+
// and the IDE all consume the same `Outcome` value, just rendered differently.
|
|
6
|
+
//
|
|
7
|
+
// See `docs/analysis/given-when-then-specifications.md` §3.2 for the canonical
|
|
8
|
+
// shape and §3.3 for the JSON rendering. Stage 7 adds sury-aware rendering and
|
|
9
|
+
// structural `fieldDiff`. Stage 4 added `AppendConditionMismatch`.
|
|
10
|
+
|
|
11
|
+
type mismatch =
|
|
12
|
+
| EventsMismatch({expected: array<JSON.t>, actual: array<JSON.t>})
|
|
13
|
+
| ErrorMismatch({
|
|
14
|
+
expected: JSON.t,
|
|
15
|
+
actual: option<JSON.t>,
|
|
16
|
+
actualEvents: array<JSON.t>,
|
|
17
|
+
})
|
|
18
|
+
| StateMismatch({key: string, expected: option<JSON.t>, actual: option<JSON.t>})
|
|
19
|
+
| NoEventExpected({actual: array<JSON.t>})
|
|
20
|
+
| TodoMismatch({expected: array<(string, JSON.t)>, actual: array<(string, JSON.t)>})
|
|
21
|
+
| AppendConditionMismatch({expected: JSON.t, actual: JSON.t})
|
|
22
|
+
| TranslateError({expected: string, actual: option<string>})
|
|
23
|
+
| QueryRowsMismatch({expected: array<JSON.t>, actual: array<JSON.t>})
|
|
24
|
+
// The set of published actions (events/commands) emitted by an ExtensionPoint
|
|
25
|
+
// mapping or an Extension delegate diverged from the expectation. Supports
|
|
26
|
+
// one-to-many fan-out (one inbound message → N published actions). Used by
|
|
27
|
+
// `Delegate_GWT` and the cross-plugin `Flow_GWT` boundary steps.
|
|
28
|
+
| PublishedActionsMismatch({expected: array<JSON.t>, actual: array<JSON.t>})
|
|
29
|
+
| Throw({error: string, stack: string})
|
|
30
|
+
|
|
31
|
+
type outcome = result<unit, mismatch>
|
|
32
|
+
|
|
33
|
+
let pass: outcome = Ok()
|
|
34
|
+
let fail = (m): outcome => Error(m)
|
|
35
|
+
|
|
36
|
+
let kindName = (m: mismatch) =>
|
|
37
|
+
switch m {
|
|
38
|
+
| EventsMismatch(_) => "EventsMismatch"
|
|
39
|
+
| ErrorMismatch(_) => "ErrorMismatch"
|
|
40
|
+
| StateMismatch(_) => "StateMismatch"
|
|
41
|
+
| NoEventExpected(_) => "NoEventExpected"
|
|
42
|
+
| TodoMismatch(_) => "TodoMismatch"
|
|
43
|
+
| AppendConditionMismatch(_) => "AppendConditionMismatch"
|
|
44
|
+
| TranslateError(_) => "TranslateError"
|
|
45
|
+
| QueryRowsMismatch(_) => "QueryRowsMismatch"
|
|
46
|
+
| PublishedActionsMismatch(_) => "PublishedActionsMismatch"
|
|
47
|
+
| Throw(_) => "Throw"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let stringifyJson = (j: JSON.t) => JSON.stringify(j, ~space=2)
|
|
51
|
+
let stringifyOptJson = (j: option<JSON.t>) =>
|
|
52
|
+
switch j {
|
|
53
|
+
| Some(v) => stringifyJson(v)
|
|
54
|
+
| None => "(none)"
|
|
55
|
+
}
|
|
56
|
+
let stringifyJsonArray = (arr: array<JSON.t>) =>
|
|
57
|
+
"[" ++ arr->Array.map(stringifyJson)->Array.join(", ") ++ "]"
|
|
58
|
+
|
|
59
|
+
let format = (m: mismatch) =>
|
|
60
|
+
switch m {
|
|
61
|
+
| EventsMismatch({expected, actual}) =>
|
|
62
|
+
`EventsMismatch:\n expected: ${stringifyJsonArray(expected)}\n actual: ${stringifyJsonArray(
|
|
63
|
+
actual,
|
|
64
|
+
)}`
|
|
65
|
+
| ErrorMismatch({expected, actual, actualEvents}) =>
|
|
66
|
+
`ErrorMismatch:\n expected error: ${stringifyJson(expected)}\n actual error: ${stringifyOptJson(
|
|
67
|
+
actual,
|
|
68
|
+
)}\n actual events: ${stringifyJsonArray(actualEvents)}`
|
|
69
|
+
| StateMismatch({key, expected, actual}) =>
|
|
70
|
+
`StateMismatch (key: ${key}):\n expected: ${stringifyOptJson(
|
|
71
|
+
expected,
|
|
72
|
+
)}\n actual: ${stringifyOptJson(actual)}`
|
|
73
|
+
| NoEventExpected({actual}) =>
|
|
74
|
+
`NoEventExpected: expected no events, got:\n ${stringifyJsonArray(actual)}`
|
|
75
|
+
| TodoMismatch({expected, actual}) => {
|
|
76
|
+
let fmt = arr =>
|
|
77
|
+
arr
|
|
78
|
+
->Array.map(((id, j)) => `(${id}, ${stringifyJson(j)})`)
|
|
79
|
+
->Array.join(", ")
|
|
80
|
+
`TodoMismatch:\n expected: [${fmt(expected)}]\n actual: [${fmt(actual)}]`
|
|
81
|
+
}
|
|
82
|
+
| AppendConditionMismatch({expected, actual}) =>
|
|
83
|
+
`AppendConditionMismatch:\n expected: ${stringifyJson(expected)}\n actual: ${stringifyJson(
|
|
84
|
+
actual,
|
|
85
|
+
)}`
|
|
86
|
+
| TranslateError({expected, actual}) =>
|
|
87
|
+
`TranslateError:\n expected: ${expected}\n actual: ${actual->Option.getOr("(none)")}`
|
|
88
|
+
| QueryRowsMismatch({expected, actual}) =>
|
|
89
|
+
`QueryRowsMismatch:\n expected: ${stringifyJsonArray(expected)}\n actual: ${stringifyJsonArray(
|
|
90
|
+
actual,
|
|
91
|
+
)}`
|
|
92
|
+
| PublishedActionsMismatch({expected, actual}) =>
|
|
93
|
+
`PublishedActionsMismatch:\n expected: ${stringifyJsonArray(expected)}\n actual: ${stringifyJsonArray(
|
|
94
|
+
actual,
|
|
95
|
+
)}`
|
|
96
|
+
| Throw({error, stack}) => `Throw: ${error}\n${stack}`
|
|
97
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
|
|
5
|
+
function fail(m) {
|
|
6
|
+
return {
|
|
7
|
+
TAG: "Error",
|
|
8
|
+
_0: m
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function kindName(m) {
|
|
13
|
+
switch (m.TAG) {
|
|
14
|
+
case "EventsMismatch" :
|
|
15
|
+
return "EventsMismatch";
|
|
16
|
+
case "ErrorMismatch" :
|
|
17
|
+
return "ErrorMismatch";
|
|
18
|
+
case "StateMismatch" :
|
|
19
|
+
return "StateMismatch";
|
|
20
|
+
case "NoEventExpected" :
|
|
21
|
+
return "NoEventExpected";
|
|
22
|
+
case "TodoMismatch" :
|
|
23
|
+
return "TodoMismatch";
|
|
24
|
+
case "AppendConditionMismatch" :
|
|
25
|
+
return "AppendConditionMismatch";
|
|
26
|
+
case "TranslateError" :
|
|
27
|
+
return "TranslateError";
|
|
28
|
+
case "QueryRowsMismatch" :
|
|
29
|
+
return "QueryRowsMismatch";
|
|
30
|
+
case "PublishedActionsMismatch" :
|
|
31
|
+
return "PublishedActionsMismatch";
|
|
32
|
+
case "Throw" :
|
|
33
|
+
return "Throw";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stringifyJson(j) {
|
|
38
|
+
return JSON.stringify(j, undefined, 2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stringifyOptJson(j) {
|
|
42
|
+
if (j !== undefined) {
|
|
43
|
+
return JSON.stringify(j, undefined, 2);
|
|
44
|
+
} else {
|
|
45
|
+
return "(none)";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function stringifyJsonArray(arr) {
|
|
50
|
+
return "[" + arr.map(stringifyJson).join(", ") + "]";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function format(m) {
|
|
54
|
+
switch (m.TAG) {
|
|
55
|
+
case "EventsMismatch" :
|
|
56
|
+
return `EventsMismatch:\n expected: ` + stringifyJsonArray(m.expected) + `\n actual: ` + stringifyJsonArray(m.actual);
|
|
57
|
+
case "ErrorMismatch" :
|
|
58
|
+
return `ErrorMismatch:\n expected error: ` + JSON.stringify(m.expected, undefined, 2) + `\n actual error: ` + stringifyOptJson(m.actual) + `\n actual events: ` + stringifyJsonArray(m.actualEvents);
|
|
59
|
+
case "StateMismatch" :
|
|
60
|
+
return `StateMismatch (key: ` + m.key + `):\n expected: ` + stringifyOptJson(m.expected) + `\n actual: ` + stringifyOptJson(m.actual);
|
|
61
|
+
case "NoEventExpected" :
|
|
62
|
+
return `NoEventExpected: expected no events, got:\n ` + stringifyJsonArray(m.actual);
|
|
63
|
+
case "TodoMismatch" :
|
|
64
|
+
let fmt = arr => arr.map(param => `(` + param[0] + `, ` + JSON.stringify(param[1], undefined, 2) + `)`).join(", ");
|
|
65
|
+
return `TodoMismatch:\n expected: [` + fmt(m.expected) + `]\n actual: [` + fmt(m.actual) + `]`;
|
|
66
|
+
case "AppendConditionMismatch" :
|
|
67
|
+
return `AppendConditionMismatch:\n expected: ` + JSON.stringify(m.expected, undefined, 2) + `\n actual: ` + JSON.stringify(m.actual, undefined, 2);
|
|
68
|
+
case "TranslateError" :
|
|
69
|
+
return `TranslateError:\n expected: ` + m.expected + `\n actual: ` + Stdlib_Option.getOr(m.actual, "(none)");
|
|
70
|
+
case "QueryRowsMismatch" :
|
|
71
|
+
return `QueryRowsMismatch:\n expected: ` + stringifyJsonArray(m.expected) + `\n actual: ` + stringifyJsonArray(m.actual);
|
|
72
|
+
case "PublishedActionsMismatch" :
|
|
73
|
+
return `PublishedActionsMismatch:\n expected: ` + stringifyJsonArray(m.expected) + `\n actual: ` + stringifyJsonArray(m.actual);
|
|
74
|
+
case "Throw" :
|
|
75
|
+
return `Throw: ` + m.error + `\n` + m.stack;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let pass = {
|
|
80
|
+
TAG: "Ok",
|
|
81
|
+
_0: undefined
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
pass,
|
|
86
|
+
fail,
|
|
87
|
+
kindName,
|
|
88
|
+
stringifyJson,
|
|
89
|
+
stringifyOptJson,
|
|
90
|
+
stringifyJsonArray,
|
|
91
|
+
format,
|
|
92
|
+
}
|
|
93
|
+
/* No side effect */
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Derives the set of workspace packages that own GWT test files, so a watch
|
|
2
|
+
// session knows which packages to keep compiling (`pnpm run start` →
|
|
3
|
+
// `rescript build -w`). Given the absolute test-file paths from `Discovery`,
|
|
4
|
+
// each is mapped to its nearest ancestor `package.json` that declares a
|
|
5
|
+
// `start` script; results are deduplicated by directory.
|
|
6
|
+
//
|
|
7
|
+
// This is the input both the `packages` NDJSON event (FormatterVsCode) and the
|
|
8
|
+
// watch-mode build manager (ProcessManager) consume — computed fresh per run so
|
|
9
|
+
// it never drifts when a plugin is added or removed.
|
|
10
|
+
|
|
11
|
+
type pkg = {
|
|
12
|
+
// Package name from package.json (may be "" if absent).
|
|
13
|
+
name: string,
|
|
14
|
+
// Absolute directory containing the owning package.json.
|
|
15
|
+
dir: string,
|
|
16
|
+
// The `start` script's command (the ReScript watch command), informational.
|
|
17
|
+
build: string,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@module("node:fs") external _existsSync: string => bool = "existsSync"
|
|
21
|
+
@module("node:fs") external _readFileSync: (string, string) => string = "readFileSync"
|
|
22
|
+
@module("node:path") external dirname: string => string = "dirname"
|
|
23
|
+
@module("node:path") external join: (string, string) => string = "join"
|
|
24
|
+
|
|
25
|
+
// Reads name + `start` command from a package.json, if it declares one.
|
|
26
|
+
let readPackage = (pkgJsonPath: string): option<(string, string)> =>
|
|
27
|
+
try {
|
|
28
|
+
let json = JSON.parseOrThrow(_readFileSync(pkgJsonPath, "utf8"))
|
|
29
|
+
switch json {
|
|
30
|
+
| Object(obj) =>
|
|
31
|
+
let name = switch obj->Dict.get("name") {
|
|
32
|
+
| Some(String(n)) => n
|
|
33
|
+
| _ => ""
|
|
34
|
+
}
|
|
35
|
+
switch obj->Dict.get("scripts") {
|
|
36
|
+
| Some(Object(scripts)) =>
|
|
37
|
+
switch scripts->Dict.get("start") {
|
|
38
|
+
| Some(String(cmd)) => Some((name, cmd))
|
|
39
|
+
| _ => None
|
|
40
|
+
}
|
|
41
|
+
| _ => None
|
|
42
|
+
}
|
|
43
|
+
| _ => None
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
| _ => None
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Walks up from `dir` to the nearest package.json declaring a `start` script.
|
|
50
|
+
let rec findOwning = (dir: string): option<pkg> => {
|
|
51
|
+
let pkgJson = join(dir, "package.json")
|
|
52
|
+
let here = _existsSync(pkgJson) ? readPackage(pkgJson) : None
|
|
53
|
+
switch here {
|
|
54
|
+
| Some((name, build)) => Some({name, dir, build})
|
|
55
|
+
| None => {
|
|
56
|
+
let parent = dirname(dir)
|
|
57
|
+
parent == dir ? None : findOwning(parent)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Maps test files → deduplicated owning packages (declaration order preserved).
|
|
63
|
+
let scan = (testFiles: array<string>): array<pkg> => {
|
|
64
|
+
let seen = Dict.make()
|
|
65
|
+
let out = []
|
|
66
|
+
testFiles->Array.forEach(file =>
|
|
67
|
+
switch findOwning(dirname(file)) {
|
|
68
|
+
| Some(pkg) =>
|
|
69
|
+
switch seen->Dict.get(pkg.dir) {
|
|
70
|
+
| Some(_) => ()
|
|
71
|
+
| None => {
|
|
72
|
+
seen->Dict.set(pkg.dir, true)
|
|
73
|
+
out->Array.push(pkg)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
| None => ()
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
out
|
|
80
|
+
}
|