97 lines
3.1 KiB
TypeScript
97 lines
3.1 KiB
TypeScript
import { Duration } from "effect"
|
|
import { OpenApiMethods, type OpenApiSpec, type Options, type Result, type Scenario } from "./types"
|
|
|
|
type ScenarioTimeout = `${number} ${Duration.Unit}`
|
|
|
|
const durationUnits = new Set<string>([
|
|
"nano",
|
|
"nanos",
|
|
"micro",
|
|
"micros",
|
|
"milli",
|
|
"millis",
|
|
"second",
|
|
"seconds",
|
|
"minute",
|
|
"minutes",
|
|
"hour",
|
|
"hours",
|
|
"day",
|
|
"days",
|
|
"week",
|
|
"weeks",
|
|
])
|
|
|
|
export function routeKeys(spec: OpenApiSpec) {
|
|
return Object.entries(spec.paths ?? {})
|
|
.flatMap(([path, item]) =>
|
|
OpenApiMethods.filter((method) => item[method]).map((method) => `${method.toUpperCase()} ${path}`),
|
|
)
|
|
.sort()
|
|
}
|
|
|
|
export function routeKey(scenario: Scenario) {
|
|
return `${scenario.method} ${scenario.path}`
|
|
}
|
|
|
|
export function coverageResult(scenario: Scenario): Result {
|
|
if (scenario.kind === "todo") return { status: "skip", scenario }
|
|
return { status: "pass", scenario }
|
|
}
|
|
|
|
export function parseOptions(args: string[]): Options {
|
|
const mode = option(args, "--mode") ?? "effect"
|
|
if (mode !== "effect" && mode !== "coverage" && mode !== "auth") throw new Error(`invalid --mode ${mode}`)
|
|
return {
|
|
mode,
|
|
include: option(args, "--include"),
|
|
startAt: option(args, "--start-at"),
|
|
stopAt: option(args, "--stop-at"),
|
|
failOnMissing: args.includes("--fail-on-missing"),
|
|
failOnSkip: args.includes("--fail-on-skip"),
|
|
scenarioTimeout: parseScenarioTimeout(option(args, "--scenario-timeout") ?? "30 seconds"),
|
|
progress: args.includes("--progress"),
|
|
trace: args.includes("--trace"),
|
|
}
|
|
}
|
|
|
|
export function matches(options: Options, scenario: Scenario) {
|
|
if (!options.include) return true
|
|
return (
|
|
scenario.name.includes(options.include) ||
|
|
scenario.path.includes(options.include) ||
|
|
scenario.method.includes(options.include.toUpperCase())
|
|
)
|
|
}
|
|
|
|
export function selectedScenarios(options: Options, scenarios: Scenario[]) {
|
|
const included = scenarios.filter((scenario) => matches(options, scenario))
|
|
const start = options.startAt ? included.findIndex((scenario) => matchesName(options.startAt!, scenario)) : 0
|
|
const end = options.stopAt
|
|
? included.findIndex((scenario) => matchesName(options.stopAt!, scenario))
|
|
: included.length - 1
|
|
if (start === -1) throw new Error(`--start-at matched no scenario: ${options.startAt}`)
|
|
if (end === -1) throw new Error(`--stop-at matched no scenario: ${options.stopAt}`)
|
|
return included.slice(start, end + 1)
|
|
}
|
|
|
|
function matchesName(value: string, scenario: Scenario) {
|
|
return scenario.name.includes(value) || scenario.path.includes(value) || scenario.method.includes(value.toUpperCase())
|
|
}
|
|
|
|
function option(args: string[], name: string) {
|
|
const index = args.indexOf(name)
|
|
if (index === -1) return undefined
|
|
return args[index + 1]
|
|
}
|
|
|
|
function parseScenarioTimeout(input: string) {
|
|
if (!isScenarioTimeout(input)) throw new Error(`invalid --scenario-timeout ${input}`)
|
|
return Duration.fromInputUnsafe(input)
|
|
}
|
|
|
|
function isScenarioTimeout(input: string): input is ScenarioTimeout {
|
|
const [amount, unit, extra] = input.trim().split(/\s+/)
|
|
return extra === undefined && amount !== undefined && Number.isFinite(Number(amount)) && durationUnits.has(unit ?? "")
|
|
}
|