432 lines
13 KiB
JavaScript
Raw Normal View History

2026-01-26 05:12:14 +01:00
// retoor <retoor@molodetz.nl>
import "os" for Process, Platform
import "pathlib" for Path
import "regex" for Regex
import "argparse" for ArgumentParser
import "subprocess" for Popen
import "io" for Stdout
class Colors {
static green(text) { Platform.isWindows ? text : "\x1b[32m%(text)\x1b[0m" }
static red(text) { Platform.isWindows ? text : "\x1b[31m%(text)\x1b[0m" }
static pink(text) { Platform.isWindows ? text : "\x1b[91m%(text)\x1b[0m" }
static yellow(text) { Platform.isWindows ? text : "\x1b[33m%(text)\x1b[0m" }
}
class Patterns {
static expectPattern { __expectPattern }
static expectErrorLinePattern { __expectErrorLinePattern }
static expectErrorPattern { __expectErrorPattern }
static expectHandledRuntimeErrorPattern { __expectHandledRuntimeErrorPattern }
static expectRuntimeErrorPattern { __expectRuntimeErrorPattern }
static stdinPattern { __stdinPattern }
static skipPattern { __skipPattern }
static nontestPattern { __nontestPattern }
static splitPattern { __splitPattern }
static errorPattern { __errorPattern }
static stackTracePattern { __stackTracePattern }
static init() {
__expectPattern = Regex.new("// expect: ?(.*)")
__expectErrorLinePattern = Regex.new("// expect error line ([0-9]+)")
__expectErrorPattern = Regex.new("// expect error")
__expectHandledRuntimeErrorPattern = Regex.new("// expect handled runtime error: (.+)")
__expectRuntimeErrorPattern = Regex.new("// expect runtime error: (.+)")
__stdinPattern = Regex.new("// stdin: (.*)")
__skipPattern = Regex.new("// skip: (.*)")
__nontestPattern = Regex.new("// nontest")
__splitPattern = Regex.new("\n|\r\n")
__errorPattern = Regex.new("\\[.* line ([0-9]+)\\] Error")
__stackTracePattern = Regex.new("test/.* line ([0-9]+)\\] in")
}
}
class Test {
construct new(path) {
_path = path
_output = []
_compileErrors = []
_runtimeErrorLine = 0
_runtimeErrorMessage = null
_exitCode = 0
_inputBytes = null
_failures = []
}
path { _path }
failures { _failures }
parse() {
var inputLines = []
var lineNum = 1
var content = Path.new(_path).readText()
var lines = Patterns.splitPattern.split(content)
for (line in lines) {
if (line.count == 0) {
lineNum = lineNum + 1
continue
}
var match = Patterns.expectPattern.match(line)
if (match) {
_output.add([match.groups[1], lineNum])
TestRunner.expectations = TestRunner.expectations + 1
}
match = Patterns.expectErrorLinePattern.match(line)
if (match) {
var errLine = Num.fromString(match.groups[1])
if (!listContains_(_compileErrors, errLine)) {
_compileErrors.add(errLine)
}
_exitCode = 65
TestRunner.expectations = TestRunner.expectations + 1
} else {
match = Patterns.expectErrorPattern.match(line)
if (match) {
if (!listContains_(_compileErrors, lineNum)) {
_compileErrors.add(lineNum)
}
_exitCode = 65
TestRunner.expectations = TestRunner.expectations + 1
}
}
match = Patterns.expectHandledRuntimeErrorPattern.match(line)
if (match) {
_runtimeErrorLine = lineNum
_runtimeErrorMessage = match.groups[1]
TestRunner.expectations = TestRunner.expectations + 1
} else {
match = Patterns.expectRuntimeErrorPattern.match(line)
if (match) {
_runtimeErrorLine = lineNum
_runtimeErrorMessage = match.groups[1]
_exitCode = 70
TestRunner.expectations = TestRunner.expectations + 1
}
}
match = Patterns.stdinPattern.match(line)
if (match) {
inputLines.add(match.groups[1])
}
match = Patterns.skipPattern.match(line)
if (match) {
TestRunner.numSkipped = TestRunner.numSkipped + 1
var reason = match.groups[1]
if (!TestRunner.skipped.containsKey(reason)) {
TestRunner.skipped[reason] = 0
}
TestRunner.skipped[reason] = TestRunner.skipped[reason] + 1
return false
}
match = Patterns.nontestPattern.match(line)
if (match) {
return false
}
lineNum = lineNum + 1
}
if (inputLines.count > 0) {
_inputBytes = inputLines.join("\n")
}
return true
}
listContains_(list, item) {
for (i in list) {
if (i == item) return true
}
return false
}
run(app, type) {
var proc = Popen.new([app, _path])
if (_inputBytes) {
proc.stdin.write(_inputBytes)
}
proc.stdin.close()
var exitCode = proc.wait()
var out = proc.stdout.read()
var err = proc.stderr.read()
validate(type == "example", exitCode, out, err)
}
validate(isExample, exitCode, out, err) {
if (_compileErrors.count > 0 && _runtimeErrorMessage) {
fail("Test error: Cannot expect both compile and runtime errors.")
return
}
out = out.replace("\r\n", "\n")
err = err.replace("\r\n", "\n")
var errorLines = Patterns.splitPattern.split(err)
if (_runtimeErrorMessage) {
validateRuntimeError(errorLines)
} else {
validateCompileErrors(errorLines)
}
validateExitCode(exitCode, errorLines)
if (isExample) return
validateOutput(out)
}
validateRuntimeError(errorLines) {
if (errorLines.count < 2) {
fail("Expected runtime error \"" + _runtimeErrorMessage + "\" and got none.")
return
}
var line = 0
while (line < errorLines.count && Patterns.errorPattern.test(errorLines[line])) {
line = line + 1
}
if (line >= errorLines.count) {
fail("Expected runtime error \"" + _runtimeErrorMessage + "\" but only found compile errors.")
return
}
if (errorLines[line] != _runtimeErrorMessage) {
fail("Expected runtime error \"" + _runtimeErrorMessage + "\" and got:")
fail(errorLines[line])
}
var match = null
var stackLines = []
for (i in (line + 1)...errorLines.count) {
stackLines.add(errorLines[i])
match = Patterns.stackTracePattern.match(errorLines[i])
if (match) break
}
if (!match) {
fail("Expected stack trace and got:")
for (stackLine in stackLines) {
fail(stackLine)
}
} else {
var stackLine = Num.fromString(match.groups[1])
if (stackLine != _runtimeErrorLine) {
fail("Expected runtime error on line %(_runtimeErrorLine) but was on line %(stackLine).")
}
}
}
validateCompileErrors(errorLines) {
var foundErrors = []
for (line in errorLines) {
var match = Patterns.errorPattern.match(line)
if (match) {
var errorLine = Num.fromString(match.groups[1])
if (listContains_(_compileErrors, errorLine)) {
if (!listContains_(foundErrors, errorLine)) {
foundErrors.add(errorLine)
}
} else {
fail("Unexpected error:")
fail(line)
}
} else if (line != "") {
fail("Unexpected output on stderr:")
fail(line)
}
}
for (expected in _compileErrors) {
if (!listContains_(foundErrors, expected)) {
fail("Missing expected error on line %(expected).")
}
}
}
validateExitCode(exitCode, errorLines) {
if (exitCode == _exitCode) return
fail("Expected return code %(_exitCode) and got %(exitCode). Stderr:")
for (line in errorLines) {
_failures.add(line)
}
}
validateOutput(out) {
var outLines = Patterns.splitPattern.split(out)
if (outLines.count > 0 && outLines[-1] == "") {
outLines = outLines[0...-1]
}
var index = 0
for (line in outLines) {
if (index >= _output.count) {
fail("Got output \"%(line)\" when none was expected.")
} else if (_output[index][0] != line) {
fail("Expected output \"%(_output[index][0])\" on line %(_output[index][1]) and got \"%(line)\".")
}
index = index + 1
}
while (index < _output.count) {
fail("Missing expected output \"%(_output[index][0])\" on line %(_output[index][1]).")
index = index + 1
}
}
fail(message) {
_failures.add(message)
}
}
class TestRunner {
static passed { __passed }
static passed=(value) { __passed = value }
static failed { __failed }
static failed=(value) { __failed = value }
static numSkipped { __numSkipped }
static numSkipped=(value) { __numSkipped = value }
static skipped { __skipped }
static skipped=(value) { __skipped = value }
static expectations { __expectations }
static expectations=(value) { __expectations = value }
construct new() {
TestRunner.passed = 0
TestRunner.failed = 0
TestRunner.numSkipped = 0
TestRunner.skipped = {}
TestRunner.expectations = 0
var parser = ArgumentParser.new()
parser.addArgument("--suffix", {"default": ""})
parser.addArgument("suite", {"required": false, "default": null})
parser.addArgument("--skip-tests", {"action": "storeTrue"})
parser.addArgument("--skip-examples", {"action": "storeTrue"})
_args = parser.parseArgs()
_wrenDir = Path.new(Process.cwd)
_wrenApp = _wrenDir / "bin" / ("wren_cli" + _args["suffix"])
}
run() {
var testDir = _wrenDir / "test"
walk(testDir) {|path| runTest(path) }
printLine()
if (TestRunner.failed == 0) {
System.print("All " + Colors.green(TestRunner.passed) + " tests passed (" + TestRunner.expectations.toString + " expectations).")
} else {
System.print(Colors.green(TestRunner.passed) + " tests passed. " + Colors.red(TestRunner.failed) + " tests failed.")
}
var sortedKeys = sortKeys_(TestRunner.skipped)
for (key in sortedKeys) {
System.print("Skipped " + Colors.yellow(TestRunner.skipped[key]) + " tests: " + key)
}
if (TestRunner.failed != 0) {
Process.exit(1)
}
}
sortKeys_(map) {
var keys = []
for (key in map.keys) {
keys.add(key)
}
keys.sort()
return keys
}
walk(dir, callback) {
var entries = dir.iterdir()
var files = []
var dirs = []
for (entry in entries) {
if (entry.isDir()) {
dirs.add(entry)
} else {
files.add(entry)
}
}
dirs.sort {|a, b| a.toString < b.toString }
files.sort {|a, b| a.toString < b.toString }
for (file in files) {
callback.call(file)
}
for (subdir in dirs) {
walk(subdir, callback)
}
}
printLine() { printLine(null) }
printLine(line) {
System.write("\x1b[2K")
System.write("\r")
if (line) {
System.write(line)
Stdout.flush()
}
}
runTest(path) {
var pathStr = path.toString
if (!pathStr.endsWith(".wren")) return
if (_args["suite"]) {
var testPath = path.relativeTo(_wrenDir / "test").toString
if (!testPath.startsWith(_args["suite"])) return
}
var appRelPath = _wrenApp.relativeTo(_wrenDir).toString
printLine("(" + appRelPath + ") Passed: " + Colors.green(TestRunner.passed) + " Failed: " + Colors.red(TestRunner.failed) + " Skipped: " + Colors.yellow(TestRunner.numSkipped) + " ")
var normalizedPath = path.relativeTo(Path.cwd).toString.replace("\\", "/")
var test = Test.new(normalizedPath)
if (!test.parse()) {
return
}
test.run(_wrenApp.toString, "test")
if (test.failures.count == 0) {
TestRunner.passed = TestRunner.passed + 1
} else {
TestRunner.failed = TestRunner.failed + 1
printLine(Colors.red("FAIL") + ": " + normalizedPath)
System.print("")
for (failure in test.failures) {
System.print(" " + Colors.pink(failure))
}
System.print("")
}
}
}
Patterns.init()
var runner = TestRunner.new()
runner.run()