effect-bdd 0.1.1 → 0.1.3
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/README.md +14 -0
- package/dist/Bdd.d.ts +0 -1
- package/dist/Bdd.js +1 -2
- package/dist/Errors.d.ts +0 -1
- package/dist/Errors.js +1 -2
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +1 -2
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -2
- package/dist/internal/cli/errors.d.ts +0 -1
- package/dist/internal/cli/errors.js +1 -2
- package/dist/internal/cli/glob.d.ts +0 -1
- package/dist/internal/cli/glob.js +1 -2
- package/dist/internal/cli/loaders.d.ts +0 -1
- package/dist/internal/cli/loaders.js +1 -2
- package/dist/internal/cli/models.d.ts +0 -1
- package/dist/internal/cli/models.js +1 -2
- package/dist/internal/cli/moduleLoader.d.ts +0 -1
- package/dist/internal/cli/moduleLoader.js +1 -2
- package/dist/internal/cli/reporter.d.ts +0 -1
- package/dist/internal/cli/reporter.js +1 -2
- package/dist/internal/cli/runner.d.ts +0 -1
- package/dist/internal/cli/runner.js +1 -2
- package/dist/internal/cli/tagExpression.d.ts +0 -1
- package/dist/internal/cli/tagExpression.js +1 -2
- package/dist/internal/cucumberCompiler.d.ts +0 -1
- package/dist/internal/cucumberCompiler.js +1 -2
- package/dist/internal/expression.d.ts +0 -1
- package/dist/internal/expression.js +1 -2
- package/dist/internal/matching.d.ts +0 -1
- package/dist/internal/matching.js +1 -2
- package/dist/internal/parser.d.ts +0 -1
- package/dist/internal/parser.js +1 -2
- package/dist/internal/runner.d.ts +0 -1
- package/dist/internal/runner.js +1 -2
- package/dist/main.d.ts +0 -1
- package/dist/main.js +1 -2
- package/package.json +4 -10
- package/dist/Bdd.d.ts.map +0 -1
- package/dist/Bdd.js.map +0 -1
- package/dist/Errors.d.ts.map +0 -1
- package/dist/Errors.js.map +0 -1
- package/dist/bin.d.ts.map +0 -1
- package/dist/bin.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal/cli/errors.d.ts.map +0 -1
- package/dist/internal/cli/errors.js.map +0 -1
- package/dist/internal/cli/glob.d.ts.map +0 -1
- package/dist/internal/cli/glob.js.map +0 -1
- package/dist/internal/cli/loaders.d.ts.map +0 -1
- package/dist/internal/cli/loaders.js.map +0 -1
- package/dist/internal/cli/models.d.ts.map +0 -1
- package/dist/internal/cli/models.js.map +0 -1
- package/dist/internal/cli/moduleLoader.d.ts.map +0 -1
- package/dist/internal/cli/moduleLoader.js.map +0 -1
- package/dist/internal/cli/reporter.d.ts.map +0 -1
- package/dist/internal/cli/reporter.js.map +0 -1
- package/dist/internal/cli/runner.d.ts.map +0 -1
- package/dist/internal/cli/runner.js.map +0 -1
- package/dist/internal/cli/tagExpression.d.ts.map +0 -1
- package/dist/internal/cli/tagExpression.js.map +0 -1
- package/dist/internal/cucumberCompiler.d.ts.map +0 -1
- package/dist/internal/cucumberCompiler.js.map +0 -1
- package/dist/internal/expression.d.ts.map +0 -1
- package/dist/internal/expression.js.map +0 -1
- package/dist/internal/matching.d.ts.map +0 -1
- package/dist/internal/matching.js.map +0 -1
- package/dist/internal/parser.d.ts.map +0 -1
- package/dist/internal/parser.js.map +0 -1
- package/dist/internal/runner.d.ts.map +0 -1
- package/dist/internal/runner.js.map +0 -1
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js.map +0 -1
- package/src/Bdd.ts +0 -575
- package/src/Errors.ts +0 -60
- package/src/bin.ts +0 -10
- package/src/index.ts +0 -155
- package/src/internal/cli/errors.ts +0 -20
- package/src/internal/cli/glob.ts +0 -37
- package/src/internal/cli/loaders.ts +0 -100
- package/src/internal/cli/models.ts +0 -118
- package/src/internal/cli/moduleLoader.ts +0 -41
- package/src/internal/cli/reporter.ts +0 -367
- package/src/internal/cli/runner.ts +0 -336
- package/src/internal/cli/tagExpression.ts +0 -173
- package/src/internal/cucumberCompiler.ts +0 -58
- package/src/internal/expression.ts +0 -103
- package/src/internal/matching.ts +0 -81
- package/src/internal/parser.ts +0 -155
- package/src/internal/runner.ts +0 -373
- package/src/main.ts +0 -169
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
import * as Arr from "effect/Array"
|
|
2
|
-
import * as Console from "effect/Console"
|
|
3
|
-
import * as Effect from "effect/Effect"
|
|
4
|
-
import * as FileSystem from "effect/FileSystem"
|
|
5
|
-
import { pipe } from "effect/Function"
|
|
6
|
-
import * as Path from "effect/Path"
|
|
7
|
-
import * as Str from "effect/String"
|
|
8
|
-
import * as Parser from "../parser.ts"
|
|
9
|
-
import { ReporterError } from "./errors.ts"
|
|
10
|
-
import type { CliDiagnostic, CliRunResult, ReporterName, ScenarioResult } from "./models.ts"
|
|
11
|
-
|
|
12
|
-
/** @internal */
|
|
13
|
-
export interface Reporter {
|
|
14
|
-
readonly name: ReporterName
|
|
15
|
-
readonly emit: (
|
|
16
|
-
result: CliRunResult
|
|
17
|
-
) => Effect.Effect<void, ReporterError, FileSystem.FileSystem | Path.Path>
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** @internal */
|
|
21
|
-
export const makeReporters = (
|
|
22
|
-
names: ReadonlyArray<ReporterName>,
|
|
23
|
-
outputFiles: {
|
|
24
|
-
readonly text?: string
|
|
25
|
-
readonly html?: string
|
|
26
|
-
readonly json?: string
|
|
27
|
-
readonly junit?: string
|
|
28
|
-
},
|
|
29
|
-
options: {
|
|
30
|
-
readonly verbose: boolean
|
|
31
|
-
}
|
|
32
|
-
): Effect.Effect<ReadonlyArray<Reporter>, ReporterError> =>
|
|
33
|
-
Effect.forEach(names, (name) => {
|
|
34
|
-
switch (name) {
|
|
35
|
-
case "text": {
|
|
36
|
-
return Effect.succeed(textReporter(outputFiles.text, options.verbose))
|
|
37
|
-
}
|
|
38
|
-
case "html": {
|
|
39
|
-
return outputFiles.html === undefined
|
|
40
|
-
? Effect.fail(new ReporterError({ message: "Reporter html requires --output-file.html" }))
|
|
41
|
-
: Effect.succeed(htmlReporter(outputFiles.html))
|
|
42
|
-
}
|
|
43
|
-
case "json": {
|
|
44
|
-
return Effect.succeed(jsonReporter(outputFiles.json))
|
|
45
|
-
}
|
|
46
|
-
case "junit": {
|
|
47
|
-
return outputFiles.junit === undefined
|
|
48
|
-
? Effect.fail(new ReporterError({ message: "Reporter junit requires --output-file.junit" }))
|
|
49
|
-
: Effect.succeed(junitReporter(outputFiles.junit))
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
/** @internal */
|
|
55
|
-
export const emitAll: (
|
|
56
|
-
reporters: ReadonlyArray<Reporter>,
|
|
57
|
-
result: CliRunResult
|
|
58
|
-
) => Effect.Effect<void, ReporterError, FileSystem.FileSystem | Path.Path> = Effect.fnUntraced(function*(
|
|
59
|
-
reporters: ReadonlyArray<Reporter>,
|
|
60
|
-
result: CliRunResult
|
|
61
|
-
) {
|
|
62
|
-
const exits = yield* Effect.forEach(
|
|
63
|
-
reporters,
|
|
64
|
-
(reporter) => Effect.exit(reporter.emit(result)),
|
|
65
|
-
{ concurrency: "unbounded" }
|
|
66
|
-
)
|
|
67
|
-
const failures = pipe(
|
|
68
|
-
exits,
|
|
69
|
-
Arr.filter((exit) => exit._tag === "Failure"),
|
|
70
|
-
Arr.map((exit) => exit.cause)
|
|
71
|
-
)
|
|
72
|
-
if (failures.length > 0) {
|
|
73
|
-
return yield* Effect.fail(
|
|
74
|
-
new ReporterError({
|
|
75
|
-
message: "One or more reporters failed",
|
|
76
|
-
cause: failures
|
|
77
|
-
})
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
const textReporter = (outputFile: string | undefined, verbose: boolean): Reporter => ({
|
|
83
|
-
name: "text",
|
|
84
|
-
emit: (result) =>
|
|
85
|
-
outputFile === undefined
|
|
86
|
-
? Console.log(renderText(result, verbose))
|
|
87
|
-
: writeFile(outputFile, renderText(result, verbose))
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
const htmlReporter = (outputFile: string): Reporter => ({
|
|
91
|
-
name: "html",
|
|
92
|
-
emit: (result) => writeFile(outputFile, renderHtml(result))
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
const jsonReporter = (outputFile: string | undefined): Reporter => ({
|
|
96
|
-
name: "json",
|
|
97
|
-
emit: (result) =>
|
|
98
|
-
outputFile === undefined
|
|
99
|
-
? Console.log(renderJson(result))
|
|
100
|
-
: writeFile(outputFile, renderJson(result))
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
const junitReporter = (outputFile: string): Reporter => ({
|
|
104
|
-
name: "junit",
|
|
105
|
-
emit: (result) => writeFile(outputFile, renderJunit(result))
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
const writeFile: (
|
|
109
|
-
outputFile: string,
|
|
110
|
-
content: string
|
|
111
|
-
) => Effect.Effect<void, ReporterError, FileSystem.FileSystem | Path.Path> = Effect.fnUntraced(function*(
|
|
112
|
-
outputFile: string,
|
|
113
|
-
content: string
|
|
114
|
-
) {
|
|
115
|
-
const fs = yield* FileSystem.FileSystem
|
|
116
|
-
const path = yield* Path.Path
|
|
117
|
-
const directory = path.dirname(outputFile)
|
|
118
|
-
if (directory !== ".") {
|
|
119
|
-
yield* fs.makeDirectory(directory, { recursive: true }).pipe(
|
|
120
|
-
Effect.mapError((cause) =>
|
|
121
|
-
new ReporterError({
|
|
122
|
-
message: `Could not create report directory "${directory}"`,
|
|
123
|
-
cause
|
|
124
|
-
})
|
|
125
|
-
)
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
yield* fs.writeFileString(outputFile, content).pipe(
|
|
129
|
-
Effect.mapError((cause) =>
|
|
130
|
-
new ReporterError({
|
|
131
|
-
message: `Could not write report file "${outputFile}"`,
|
|
132
|
-
cause
|
|
133
|
-
})
|
|
134
|
-
)
|
|
135
|
-
)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
const renderText = (result: CliRunResult, verbose: boolean): string => {
|
|
139
|
-
const summary = [
|
|
140
|
-
`Features: ${result.summary.features}, Scenarios: ${result.summary.total}, passed: ${result.summary.passed}, failed: ${result.summary.failed}`,
|
|
141
|
-
`Duration: ${result.summary.durationMillis}ms`,
|
|
142
|
-
""
|
|
143
|
-
]
|
|
144
|
-
const scenarioLines = verbose
|
|
145
|
-
? Arr.map(result.results, renderScenarioText)
|
|
146
|
-
: pipe(
|
|
147
|
-
result.results,
|
|
148
|
-
Arr.filter((scenario) => scenario.outcome._tag === "Failed"),
|
|
149
|
-
Arr.map(renderScenarioText)
|
|
150
|
-
)
|
|
151
|
-
const diagnosticLines = renderDiagnosticsText(result.diagnostics)
|
|
152
|
-
return pipe(
|
|
153
|
-
summary,
|
|
154
|
-
Arr.appendAll(scenarioLines),
|
|
155
|
-
Arr.appendAll(diagnosticLines),
|
|
156
|
-
Arr.join("\n")
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const renderScenarioText = (result: ScenarioResult): string => {
|
|
161
|
-
const prefix = result.outcome._tag === "Passed" ? "PASS" : "FAIL"
|
|
162
|
-
const base = `${prefix} ${result.task.featurePath}:${result.task.core.scenarioLine} ${
|
|
163
|
-
renderScenarioName(result)
|
|
164
|
-
} (${result.durationMillis}ms)`
|
|
165
|
-
return result.outcome._tag === "Passed"
|
|
166
|
-
? base
|
|
167
|
-
: `${base}\n ${renderError(result.outcome.error)}`
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const renderHtml = (result: CliRunResult): string =>
|
|
171
|
-
`<!doctype html>
|
|
172
|
-
<html lang="en">
|
|
173
|
-
<head>
|
|
174
|
-
<meta charset="utf-8">
|
|
175
|
-
<title>effect-bdd report</title>
|
|
176
|
-
<style>
|
|
177
|
-
body { font-family: system-ui, sans-serif; margin: 2rem; }
|
|
178
|
-
table { border-collapse: collapse; width: 100%; }
|
|
179
|
-
th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; }
|
|
180
|
-
.passed { color: #166534; }
|
|
181
|
-
.failed { color: #991b1b; }
|
|
182
|
-
</style>
|
|
183
|
-
</head>
|
|
184
|
-
<body>
|
|
185
|
-
<h1>effect-bdd report</h1>
|
|
186
|
-
<p>Features: ${result.summary.features}, scenarios: ${result.summary.total}, passed: ${result.summary.passed}, failed: ${result.summary.failed}</p>
|
|
187
|
-
<p>Duration: ${result.summary.durationMillis}ms</p>
|
|
188
|
-
<table>
|
|
189
|
-
<thead>
|
|
190
|
-
<tr><th>Status</th><th>Source</th><th>Feature</th><th>Scenario</th><th>Tags</th><th>Duration</th><th>Error</th></tr>
|
|
191
|
-
</thead>
|
|
192
|
-
<tbody>
|
|
193
|
-
${pipe(result.results, Arr.map(renderScenarioHtml), Arr.join("\n"))}
|
|
194
|
-
</tbody>
|
|
195
|
-
</table>
|
|
196
|
-
<h2>Diagnostics</h2>
|
|
197
|
-
<pre>${escapeHtml(pipe(renderDiagnosticsText(result.diagnostics), Arr.join("\n")))}</pre>
|
|
198
|
-
</body>
|
|
199
|
-
</html>
|
|
200
|
-
`
|
|
201
|
-
|
|
202
|
-
const renderScenarioHtml = (result: ScenarioResult): string => {
|
|
203
|
-
const status = result.outcome._tag === "Passed" ? "passed" : "failed"
|
|
204
|
-
const error = result.outcome._tag === "Passed"
|
|
205
|
-
? ""
|
|
206
|
-
: renderError(result.outcome.error)
|
|
207
|
-
return ` <tr>
|
|
208
|
-
<td class="${status}">${status}</td>
|
|
209
|
-
<td>${escapeHtml(`${result.task.featurePath}:${result.task.core.scenarioLine}`)}</td>
|
|
210
|
-
<td>${escapeHtml(result.task.core.featureName)}</td>
|
|
211
|
-
<td>${escapeHtml(renderScenarioName(result))}</td>
|
|
212
|
-
<td>${escapeHtml(pipe(result.task.core.tags, Arr.join(", ")))}</td>
|
|
213
|
-
<td>${result.durationMillis}ms</td>
|
|
214
|
-
<td>${escapeHtml(error)}</td>
|
|
215
|
-
</tr>`
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const renderDiagnosticsText = (diagnostics: ReadonlyArray<CliDiagnostic>): ReadonlyArray<string> => {
|
|
219
|
-
if (diagnostics.length === 0) {
|
|
220
|
-
return []
|
|
221
|
-
}
|
|
222
|
-
const unmatched = pipe(
|
|
223
|
-
diagnostics,
|
|
224
|
-
Arr.filter((diagnostic) =>
|
|
225
|
-
diagnostic._tag === "UnmatchedFeature" ||
|
|
226
|
-
diagnostic._tag === "UnmatchedScenario" ||
|
|
227
|
-
diagnostic._tag === "UnmatchedStep"
|
|
228
|
-
)
|
|
229
|
-
)
|
|
230
|
-
const unused = pipe(
|
|
231
|
-
diagnostics,
|
|
232
|
-
Arr.filter((diagnostic) =>
|
|
233
|
-
diagnostic._tag === "UnusedFeatureDefinition" ||
|
|
234
|
-
diagnostic._tag === "UnusedStepDefinition"
|
|
235
|
-
)
|
|
236
|
-
)
|
|
237
|
-
return pipe(
|
|
238
|
-
unmatched.length === 0 ? [] : ["", "Unmatched source:"],
|
|
239
|
-
Arr.appendAll(Arr.map(unmatched, renderDiagnosticText)),
|
|
240
|
-
Arr.appendAll(unused.length === 0 ? [] : ["", "Unused definitions:"]),
|
|
241
|
-
Arr.appendAll(Arr.map(unused, renderDiagnosticText))
|
|
242
|
-
)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const renderDiagnosticText = (diagnostic: CliDiagnostic): string => {
|
|
246
|
-
switch (diagnostic._tag) {
|
|
247
|
-
case "UnmatchedFeature": {
|
|
248
|
-
return ` ${diagnostic.featurePath}:${diagnostic.line}\n Feature: ${diagnostic.featureName}\n Reason: ${diagnostic.message}`
|
|
249
|
-
}
|
|
250
|
-
case "UnmatchedScenario": {
|
|
251
|
-
return ` ${diagnostic.featurePath}:${diagnostic.scenarioLine}\n Scenario: ${diagnostic.scenarioName}\n Reason: ${diagnostic.message}`
|
|
252
|
-
}
|
|
253
|
-
case "UnmatchedStep": {
|
|
254
|
-
return ` ${diagnostic.featurePath}:${
|
|
255
|
-
Parser.stepLine(diagnostic.step, diagnostic.source)
|
|
256
|
-
}\n Scenario: ${diagnostic.featureName} / ${diagnostic.scenarioName}\n Step: ${
|
|
257
|
-
Parser.stepKeyword(diagnostic.step, diagnostic.source)
|
|
258
|
-
} ${diagnostic.step.text}\n Reason: ${diagnostic.message}`
|
|
259
|
-
}
|
|
260
|
-
case "UnusedFeatureDefinition": {
|
|
261
|
-
return ` ${diagnostic.message}`
|
|
262
|
-
}
|
|
263
|
-
case "UnusedStepDefinition": {
|
|
264
|
-
return ` ${diagnostic.message}`
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const renderJson = (result: CliRunResult): string =>
|
|
270
|
-
JSON.stringify(
|
|
271
|
-
{
|
|
272
|
-
summary: result.summary,
|
|
273
|
-
scenarios: Arr.map(result.results, (scenario) => ({
|
|
274
|
-
source: {
|
|
275
|
-
path: scenario.task.featurePath,
|
|
276
|
-
line: scenario.task.core.scenarioLine
|
|
277
|
-
},
|
|
278
|
-
feature: scenario.task.core.featureName,
|
|
279
|
-
rule: scenario.task.core.ruleName === undefined
|
|
280
|
-
? undefined
|
|
281
|
-
: {
|
|
282
|
-
name: scenario.task.core.ruleName,
|
|
283
|
-
line: scenario.task.core.ruleLine
|
|
284
|
-
},
|
|
285
|
-
scenario: scenario.task.core.scenarioName,
|
|
286
|
-
tags: scenario.task.core.tags,
|
|
287
|
-
durationMillis: scenario.durationMillis,
|
|
288
|
-
outcome: scenario.outcome._tag === "Passed"
|
|
289
|
-
? {
|
|
290
|
-
status: "passed",
|
|
291
|
-
steps: scenario.outcome.steps
|
|
292
|
-
}
|
|
293
|
-
: {
|
|
294
|
-
status: "failed",
|
|
295
|
-
error: renderError(scenario.outcome.error)
|
|
296
|
-
}
|
|
297
|
-
})),
|
|
298
|
-
diagnostics: result.diagnostics
|
|
299
|
-
},
|
|
300
|
-
null,
|
|
301
|
-
2
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
const renderJunit = (result: CliRunResult): string => {
|
|
305
|
-
const diagnostics = result.diagnostics.length
|
|
306
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
307
|
-
<testsuite name="effect-bdd" tests="${result.summary.total + diagnostics}" failures="${
|
|
308
|
-
result.summary.failed + diagnostics
|
|
309
|
-
}" time="${result.summary.durationMillis / 1000}">
|
|
310
|
-
${pipe(result.results, Arr.map(renderJunitScenario), Arr.join("\n"))}
|
|
311
|
-
${pipe(result.diagnostics, Arr.map(renderJunitDiagnostic), Arr.join("\n"))}
|
|
312
|
-
</testsuite>
|
|
313
|
-
`
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const renderJunitScenario = (result: ScenarioResult): string => {
|
|
317
|
-
const name = renderScenarioName(result)
|
|
318
|
-
const failure = result.outcome._tag === "Passed"
|
|
319
|
-
? ""
|
|
320
|
-
: `
|
|
321
|
-
<failure message="${escapeXml(renderError(result.outcome.error))}">${
|
|
322
|
-
escapeXml(renderError(result.outcome.error))
|
|
323
|
-
}</failure>`
|
|
324
|
-
return ` <testcase classname="${escapeXml(result.task.core.featureName)}" name="${escapeXml(name)}" file="${
|
|
325
|
-
escapeXml(result.task.featurePath)
|
|
326
|
-
}" line="${result.task.core.scenarioLine}" time="${result.durationMillis / 1000}">${failure}
|
|
327
|
-
</testcase>`
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const renderJunitDiagnostic = (diagnostic: CliDiagnostic): string =>
|
|
331
|
-
` <testcase classname="effect-bdd diagnostics" name="${escapeXml(diagnostic.message)}">
|
|
332
|
-
<failure message="${escapeXml(diagnostic.message)}">${escapeXml(renderDiagnosticText(diagnostic))}</failure>
|
|
333
|
-
</testcase>`
|
|
334
|
-
|
|
335
|
-
const renderScenarioName = (result: ScenarioResult): string =>
|
|
336
|
-
result.task.core.ruleName === undefined
|
|
337
|
-
? `${result.task.core.featureName} / ${result.task.core.scenarioName}`
|
|
338
|
-
: `${result.task.core.featureName} / ${result.task.core.ruleName} / ${result.task.core.scenarioName}`
|
|
339
|
-
|
|
340
|
-
const renderError = (error: { readonly _tag: string; readonly message: string; readonly cause?: unknown }): string => {
|
|
341
|
-
const cause = renderCause(error.cause)
|
|
342
|
-
return cause === undefined
|
|
343
|
-
? `${error._tag}: ${error.message}`
|
|
344
|
-
: `${error._tag}: ${error.message}\n Cause: ${cause}`
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const renderCause = (cause: unknown): string | undefined => {
|
|
348
|
-
if (cause === undefined) {
|
|
349
|
-
return undefined
|
|
350
|
-
}
|
|
351
|
-
if (typeof cause === "object" && cause !== null && "message" in cause && typeof cause.message === "string") {
|
|
352
|
-
return cause.message
|
|
353
|
-
}
|
|
354
|
-
return String(cause)
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const escapeHtml = (text: string): string =>
|
|
358
|
-
pipe(
|
|
359
|
-
text,
|
|
360
|
-
Str.replaceAll("&", "&"),
|
|
361
|
-
Str.replaceAll("<", "<"),
|
|
362
|
-
Str.replaceAll(">", ">"),
|
|
363
|
-
Str.replaceAll("\"", """),
|
|
364
|
-
Str.replaceAll("'", "'")
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
const escapeXml = escapeHtml
|
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
import * as Arr from "effect/Array"
|
|
2
|
-
import * as Clock from "effect/Clock"
|
|
3
|
-
import * as Effect from "effect/Effect"
|
|
4
|
-
import type * as FileSystem from "effect/FileSystem"
|
|
5
|
-
import { pipe } from "effect/Function"
|
|
6
|
-
import * as Option from "effect/Option"
|
|
7
|
-
import type * as Path from "effect/Path"
|
|
8
|
-
import type * as Bdd from "../../Bdd.ts"
|
|
9
|
-
import type { ParseError } from "../../Errors.ts"
|
|
10
|
-
import * as Matching from "../matching.ts"
|
|
11
|
-
import * as Parser from "../parser.ts"
|
|
12
|
-
import * as CoreRunner from "../runner.ts"
|
|
13
|
-
import { DiscoveryError, type ModuleLoadError } from "./errors.ts"
|
|
14
|
-
import type { GlobResolver } from "./glob.ts"
|
|
15
|
-
import { loadFeatureDefinitions, loadFeatureSources } from "./loaders.ts"
|
|
16
|
-
import type {
|
|
17
|
-
CliDiagnostic,
|
|
18
|
-
CliOptions,
|
|
19
|
-
CliRunResult,
|
|
20
|
-
FeatureSource,
|
|
21
|
-
RunSummary,
|
|
22
|
-
ScenarioResult,
|
|
23
|
-
ScenarioTask
|
|
24
|
-
} from "./models.ts"
|
|
25
|
-
import type { ModuleLoader } from "./moduleLoader.ts"
|
|
26
|
-
import * as TagExpression from "./tagExpression.ts"
|
|
27
|
-
|
|
28
|
-
interface BuiltScenarios {
|
|
29
|
-
readonly tasks: ReadonlyArray<ScenarioTask>
|
|
30
|
-
readonly diagnostics: ReadonlyArray<CliDiagnostic>
|
|
31
|
-
readonly matchedFeatureNames: ReadonlyArray<string>
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface StepCoverage {
|
|
35
|
-
readonly diagnostics: ReadonlyArray<CliDiagnostic>
|
|
36
|
-
readonly usedTransitionKeys: ReadonlyArray<string>
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** @internal */
|
|
40
|
-
export const run: (
|
|
41
|
-
options: CliOptions
|
|
42
|
-
) => Effect.Effect<
|
|
43
|
-
CliRunResult,
|
|
44
|
-
CliRunError,
|
|
45
|
-
FileSystem.FileSystem | GlobResolver | ModuleLoader | Path.Path | Parser.GherkinCompiler
|
|
46
|
-
> = Effect.fnUntraced(function*(options: CliOptions) {
|
|
47
|
-
const startedAt = yield* Clock.currentTimeMillis
|
|
48
|
-
const sources = yield* loadFeatureSources(options.features)
|
|
49
|
-
const definitions = yield* loadFeatureDefinitions(options.steps)
|
|
50
|
-
const built = yield* pipe(
|
|
51
|
-
sources,
|
|
52
|
-
Effect.forEach((source) => buildScenarioTasks(source, definitions)),
|
|
53
|
-
Effect.map(combineBuiltScenarios)
|
|
54
|
-
)
|
|
55
|
-
const filteredTasks = yield* filterTasks(options, built.tasks)
|
|
56
|
-
const coverage = collectStepCoverage(filteredTasks)
|
|
57
|
-
const results = yield* runScenarios(options, filteredTasks)
|
|
58
|
-
const finishedAt = yield* Clock.currentTimeMillis
|
|
59
|
-
const diagnostics: ReadonlyArray<CliDiagnostic> = pipe(
|
|
60
|
-
built.diagnostics,
|
|
61
|
-
Arr.appendAll(coverage.diagnostics),
|
|
62
|
-
Arr.appendAll(unusedFeatureDefinitions(definitions, built.matchedFeatureNames)),
|
|
63
|
-
Arr.appendAll(
|
|
64
|
-
hasScenarioFilter(options)
|
|
65
|
-
? []
|
|
66
|
-
: unusedStepDefinitions(definitions, built.matchedFeatureNames, coverage.usedTransitionKeys)
|
|
67
|
-
)
|
|
68
|
-
)
|
|
69
|
-
return {
|
|
70
|
-
results,
|
|
71
|
-
diagnostics,
|
|
72
|
-
summary: summarize(sources.length, results, finishedAt - startedAt)
|
|
73
|
-
} satisfies CliRunResult
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
const buildScenarioTasks: (
|
|
77
|
-
source: FeatureSource,
|
|
78
|
-
definitions: ReadonlyArray<Bdd.Feature<unknown, unknown, never>>
|
|
79
|
-
) => Effect.Effect<BuiltScenarios, DiscoveryError | ParseError, Parser.GherkinCompiler> = Effect.fnUntraced(function*(
|
|
80
|
-
source: FeatureSource,
|
|
81
|
-
definitions: ReadonlyArray<Bdd.Feature<unknown, unknown, never>>
|
|
82
|
-
) {
|
|
83
|
-
const parsed = yield* Parser.parse(source.source, source.path)
|
|
84
|
-
const matches = Arr.filter(definitions, (definition) => definition.name === parsed.name)
|
|
85
|
-
if (matches.length > 1) {
|
|
86
|
-
return yield* Effect.fail(
|
|
87
|
-
new DiscoveryError({
|
|
88
|
-
message: `Multiple feature definitions matched "${parsed.name}"`
|
|
89
|
-
})
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
const definition = matches[0]
|
|
93
|
-
if (definition === undefined) {
|
|
94
|
-
return {
|
|
95
|
-
tasks: [],
|
|
96
|
-
diagnostics: pipe(
|
|
97
|
-
[
|
|
98
|
-
{
|
|
99
|
-
_tag: "UnmatchedFeature",
|
|
100
|
-
featurePath: source.path,
|
|
101
|
-
featureName: parsed.name,
|
|
102
|
-
line: parsed.line,
|
|
103
|
-
message: `Feature file has no matching Bdd.feature export: ${parsed.name}`
|
|
104
|
-
} satisfies CliDiagnostic
|
|
105
|
-
],
|
|
106
|
-
Arr.appendAll(Arr.map(parsed.pickles, (pickle): CliDiagnostic => ({
|
|
107
|
-
_tag: "UnmatchedScenario",
|
|
108
|
-
featurePath: source.path,
|
|
109
|
-
featureName: parsed.name,
|
|
110
|
-
scenarioName: pickle.name,
|
|
111
|
-
scenarioLine: pickle.location?.line ?? parsed.line,
|
|
112
|
-
message: `Scenario cannot run because no feature definition matched "${parsed.name}"`
|
|
113
|
-
})))
|
|
114
|
-
),
|
|
115
|
-
matchedFeatureNames: []
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return pipe(
|
|
119
|
-
CoreRunner.buildScenarioTasks(definition, parsed),
|
|
120
|
-
Arr.map((task): ScenarioTask => ({
|
|
121
|
-
featurePath: source.path,
|
|
122
|
-
core: task
|
|
123
|
-
})),
|
|
124
|
-
(tasks): BuiltScenarios => ({
|
|
125
|
-
tasks,
|
|
126
|
-
diagnostics: [],
|
|
127
|
-
matchedFeatureNames: [definition.name]
|
|
128
|
-
})
|
|
129
|
-
)
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
const runScenario = Effect.fnUntraced(function*(task: ScenarioTask) {
|
|
133
|
-
const startedAt = yield* Clock.currentTimeMillis
|
|
134
|
-
const result = yield* Effect.result(CoreRunner.runScenarioTask(task.core))
|
|
135
|
-
const finishedAt = yield* Clock.currentTimeMillis
|
|
136
|
-
return {
|
|
137
|
-
task,
|
|
138
|
-
outcome: result._tag === "Success"
|
|
139
|
-
? { _tag: "Passed", steps: result.success.steps }
|
|
140
|
-
: { _tag: "Failed", error: result.failure },
|
|
141
|
-
durationMillis: finishedAt - startedAt
|
|
142
|
-
} satisfies ScenarioResult
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
const runScenarios = (
|
|
146
|
-
options: CliOptions,
|
|
147
|
-
tasks: ReadonlyArray<ScenarioTask>
|
|
148
|
-
): Effect.Effect<ReadonlyArray<ScenarioResult>, never, never> =>
|
|
149
|
-
options.filters.failFast
|
|
150
|
-
? runScenariosFailFast(tasks, [])
|
|
151
|
-
: Effect.forEach(tasks, runScenario, { concurrency: options.parallel })
|
|
152
|
-
|
|
153
|
-
const runScenariosFailFast = (
|
|
154
|
-
tasks: ReadonlyArray<ScenarioTask>,
|
|
155
|
-
results: ReadonlyArray<ScenarioResult>,
|
|
156
|
-
index = 0
|
|
157
|
-
): Effect.Effect<ReadonlyArray<ScenarioResult>, never, never> => {
|
|
158
|
-
if (index >= tasks.length) {
|
|
159
|
-
return Effect.succeed(results)
|
|
160
|
-
}
|
|
161
|
-
return pipe(
|
|
162
|
-
runScenario(tasks[index]),
|
|
163
|
-
Effect.flatMap((result) =>
|
|
164
|
-
result.outcome._tag === "Failed"
|
|
165
|
-
? Effect.succeed(Arr.append(results, result))
|
|
166
|
-
: runScenariosFailFast(tasks, Arr.append(results, result), index + 1)
|
|
167
|
-
)
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const filterTasks = (
|
|
172
|
-
options: CliOptions,
|
|
173
|
-
tasks: ReadonlyArray<ScenarioTask>
|
|
174
|
-
): Effect.Effect<ReadonlyArray<ScenarioTask>, DiscoveryError> =>
|
|
175
|
-
pipe(
|
|
176
|
-
TagExpression.compileAll(options.filters.tags),
|
|
177
|
-
Effect.map((tagPredicate) => {
|
|
178
|
-
const filtered = Arr.filter(tasks, (task) =>
|
|
179
|
-
tagPredicate(task.core.tags) && matchesNameFilter(options.filters.names, task))
|
|
180
|
-
return filtered
|
|
181
|
-
}),
|
|
182
|
-
Effect.flatMap((filtered) =>
|
|
183
|
-
tasks.length > 0 && filtered.length === 0
|
|
184
|
-
? Effect.fail(new DiscoveryError({ message: "No scenarios matched the provided filters" }))
|
|
185
|
-
: Effect.succeed(filtered)
|
|
186
|
-
)
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
const matchesNameFilter = (patterns: ReadonlyArray<string>, task: ScenarioTask): boolean =>
|
|
190
|
-
patterns.length === 0 ||
|
|
191
|
-
Arr.some(patterns, (pattern) => `${task.core.featureName} / ${task.core.scenarioName}`.includes(pattern))
|
|
192
|
-
|
|
193
|
-
const hasScenarioFilter = (options: CliOptions): boolean =>
|
|
194
|
-
options.filters.tags.length > 0 || options.filters.names.length > 0
|
|
195
|
-
|
|
196
|
-
const summarize = (features: number, results: ReadonlyArray<ScenarioResult>, durationMillis: number): RunSummary => {
|
|
197
|
-
const failed = Arr.filter(results, (result) => result.outcome._tag === "Failed").length
|
|
198
|
-
return {
|
|
199
|
-
features,
|
|
200
|
-
total: results.length,
|
|
201
|
-
passed: results.length - failed,
|
|
202
|
-
failed,
|
|
203
|
-
durationMillis
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const combineBuiltScenarios = (built: ReadonlyArray<BuiltScenarios>): BuiltScenarios => ({
|
|
208
|
-
tasks: pipe(built, Arr.flatMap((item) => item.tasks)),
|
|
209
|
-
diagnostics: pipe(built, Arr.flatMap((item) => item.diagnostics)),
|
|
210
|
-
matchedFeatureNames: pipe(
|
|
211
|
-
built,
|
|
212
|
-
Arr.flatMap((item) => item.matchedFeatureNames),
|
|
213
|
-
Arr.dedupe
|
|
214
|
-
)
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
const collectStepCoverage = (tasks: ReadonlyArray<ScenarioTask>): StepCoverage =>
|
|
218
|
-
pipe(
|
|
219
|
-
tasks,
|
|
220
|
-
Arr.reduce({ diagnostics: [], usedTransitionKeys: [] } as StepCoverage, (coverage, task) =>
|
|
221
|
-
pipe(
|
|
222
|
-
task.core.pickle.steps,
|
|
223
|
-
Arr.reduce(coverage, (coverage, step) => appendStepCoverage(coverage, task, step))
|
|
224
|
-
))
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
const appendStepCoverage = (
|
|
228
|
-
coverage: StepCoverage,
|
|
229
|
-
task: ScenarioTask,
|
|
230
|
-
step: CoreRunner.ScenarioTask<unknown, unknown, never>["pickle"]["steps"][number]
|
|
231
|
-
): StepCoverage => {
|
|
232
|
-
const kind = Matching.concreteStepKind(step)
|
|
233
|
-
const textMatches = Matching.matchingTextTransitions(task.core.featureDefinition.transitions, step.text)
|
|
234
|
-
const matches = pipe(
|
|
235
|
-
kind,
|
|
236
|
-
Option.match({
|
|
237
|
-
onNone: () => [],
|
|
238
|
-
onSome: (kind) => Matching.matchingKeywordTransitions(textMatches, kind)
|
|
239
|
-
})
|
|
240
|
-
)
|
|
241
|
-
if (matches.length === 0) {
|
|
242
|
-
return {
|
|
243
|
-
diagnostics: Arr.append(coverage.diagnostics, {
|
|
244
|
-
_tag: "UnmatchedStep",
|
|
245
|
-
featurePath: task.featurePath,
|
|
246
|
-
featureName: task.core.featureName,
|
|
247
|
-
scenarioName: task.core.scenarioName,
|
|
248
|
-
scenarioLine: task.core.scenarioLine,
|
|
249
|
-
step,
|
|
250
|
-
source: task.core.source,
|
|
251
|
-
reason: textMatches.length === 0 ? "NoMatch" : "WrongKeyword",
|
|
252
|
-
candidates: textMatches.length === 0
|
|
253
|
-
? Arr.map(task.core.featureDefinition.transitions, (transition) => transition.expression.source)
|
|
254
|
-
: Arr.map(textMatches, (match) => match.transition.expression.source),
|
|
255
|
-
message: textMatches.length === 0
|
|
256
|
-
? `No transition matched step "${step.text}"`
|
|
257
|
-
: `No ${
|
|
258
|
-
pipe(kind, Option.getOrElse(() => "Step"))
|
|
259
|
-
} transition matched step "${step.text}"; matching text exists for ${
|
|
260
|
-
Matching.renderTransitionKinds(Arr.map(textMatches, (match) => match.transition))
|
|
261
|
-
}`
|
|
262
|
-
}),
|
|
263
|
-
usedTransitionKeys: coverage.usedTransitionKeys
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
if (matches.length > 1) {
|
|
267
|
-
return {
|
|
268
|
-
diagnostics: Arr.append(coverage.diagnostics, {
|
|
269
|
-
_tag: "UnmatchedStep",
|
|
270
|
-
featurePath: task.featurePath,
|
|
271
|
-
featureName: task.core.featureName,
|
|
272
|
-
scenarioName: task.core.scenarioName,
|
|
273
|
-
scenarioLine: task.core.scenarioLine,
|
|
274
|
-
step,
|
|
275
|
-
source: task.core.source,
|
|
276
|
-
reason: "MultipleMatches",
|
|
277
|
-
candidates: Arr.map(matches, (match) => match.transition.expression.source),
|
|
278
|
-
message: `Multiple transitions matched step "${step.text}"`
|
|
279
|
-
}),
|
|
280
|
-
usedTransitionKeys: coverage.usedTransitionKeys
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
return {
|
|
284
|
-
diagnostics: coverage.diagnostics,
|
|
285
|
-
usedTransitionKeys: pipe(
|
|
286
|
-
coverage.usedTransitionKeys,
|
|
287
|
-
Arr.append(transitionKey(task.core.featureName, matches[0].transition)),
|
|
288
|
-
Arr.dedupe
|
|
289
|
-
)
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const unusedFeatureDefinitions = (
|
|
294
|
-
definitions: ReadonlyArray<Bdd.Feature<unknown, unknown, never>>,
|
|
295
|
-
matchedFeatureNames: ReadonlyArray<string>
|
|
296
|
-
): ReadonlyArray<CliDiagnostic> =>
|
|
297
|
-
pipe(
|
|
298
|
-
definitions,
|
|
299
|
-
Arr.filter((definition) => !Arr.contains(definition.name)(matchedFeatureNames)),
|
|
300
|
-
Arr.map((definition): CliDiagnostic => ({
|
|
301
|
-
_tag: "UnusedFeatureDefinition",
|
|
302
|
-
featureName: definition.name,
|
|
303
|
-
message: `Feature definition exported but no feature file matched: ${definition.name}`
|
|
304
|
-
}))
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
const unusedStepDefinitions = (
|
|
308
|
-
definitions: ReadonlyArray<Bdd.Feature<unknown, unknown, never>>,
|
|
309
|
-
matchedFeatureNames: ReadonlyArray<string>,
|
|
310
|
-
usedTransitionKeys: ReadonlyArray<string>
|
|
311
|
-
): ReadonlyArray<CliDiagnostic> =>
|
|
312
|
-
pipe(
|
|
313
|
-
definitions,
|
|
314
|
-
Arr.filter((definition) => Arr.contains(definition.name)(matchedFeatureNames)),
|
|
315
|
-
Arr.flatMap((definition) =>
|
|
316
|
-
pipe(
|
|
317
|
-
definition.transitions,
|
|
318
|
-
Arr.filter((transition) => !Arr.contains(transitionKey(definition.name, transition))(usedTransitionKeys)),
|
|
319
|
-
Arr.map((transition): CliDiagnostic => ({
|
|
320
|
-
_tag: "UnusedStepDefinition",
|
|
321
|
-
featureName: definition.name,
|
|
322
|
-
expression: transition.expression.source,
|
|
323
|
-
kind: transition.kind,
|
|
324
|
-
message: `Step definition never matched: ${transition.expression.source}`
|
|
325
|
-
}))
|
|
326
|
-
)
|
|
327
|
-
)
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
const transitionKey = (
|
|
331
|
-
featureName: string,
|
|
332
|
-
transition: Bdd.Transition<unknown, unknown, never>
|
|
333
|
-
): string => `${featureName}:${transition.kind}:${transition.expression.source}`
|
|
334
|
-
|
|
335
|
-
/** @internal */
|
|
336
|
-
export type CliRunError = DiscoveryError | ModuleLoadError | ParseError
|