From 0730b8125472b3607633d04367d3a20592b3408c Mon Sep 17 00:00:00 2001 From: Karl Larsaeus Date: Sat, 27 Nov 2021 15:55:15 +0100 Subject: [PATCH] Add tests, testrunner and gradle task `run-tests` --- README.md | 15 +- bin/krisp-repl.sh | 4 + build.gradle.kts | 8 +- .../kotlin/com/ninjacontrol/krisp/Main.kt | 9 +- .../kotlin/com/ninjacontrol/krisp/AllTests.kt | 32 +++ .../com/ninjacontrol/krisp/Assertions.kt | 79 +++++++ .../com/ninjacontrol/krisp/EnvironmentTest.kt | 164 +++++++++++++ .../com/ninjacontrol/krisp/EvaluationTest.kt | 50 ++++ .../com/ninjacontrol/krisp/FunctionsTest.kt | 44 ++++ .../com/ninjacontrol/krisp/MacroTest.kt | 24 ++ .../com/ninjacontrol/krisp/MetadataTest.kt | 34 +++ .../com/ninjacontrol/krisp/NamespaceTest.kt | 222 ++++++++++++++++++ .../com/ninjacontrol/krisp/QuoteTest.kt | 64 +++++ .../com/ninjacontrol/krisp/StringTest.kt | 89 +++++++ .../com/ninjacontrol/krisp/TestRunner.kt | 29 +++ .../com/ninjacontrol/krisp/TestSuite.kt | 90 +++++++ 16 files changed, 949 insertions(+), 8 deletions(-) create mode 100755 bin/krisp-repl.sh create mode 100644 src/test/kotlin/com/ninjacontrol/krisp/AllTests.kt create mode 100644 src/test/kotlin/com/ninjacontrol/krisp/Assertions.kt create mode 100644 src/test/kotlin/com/ninjacontrol/krisp/EnvironmentTest.kt create mode 100644 src/test/kotlin/com/ninjacontrol/krisp/EvaluationTest.kt create mode 100644 src/test/kotlin/com/ninjacontrol/krisp/FunctionsTest.kt create mode 100644 src/test/kotlin/com/ninjacontrol/krisp/MacroTest.kt create mode 100644 src/test/kotlin/com/ninjacontrol/krisp/MetadataTest.kt create mode 100644 src/test/kotlin/com/ninjacontrol/krisp/NamespaceTest.kt create mode 100644 src/test/kotlin/com/ninjacontrol/krisp/QuoteTest.kt create mode 100644 src/test/kotlin/com/ninjacontrol/krisp/StringTest.kt create mode 100644 src/test/kotlin/com/ninjacontrol/krisp/TestRunner.kt create mode 100644 src/test/kotlin/com/ninjacontrol/krisp/TestSuite.kt diff --git a/README.md b/README.md index e740adb..f4f3184 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,15 @@ # krisp -A Lisp in Kotlin + +A Lisp in Kotlin + +## Build + + $ gradle build + +## Start (REPL) + + $ bin/krisp-repl.sh + +## Run tests + + $ gradle run-tests \ No newline at end of file diff --git a/bin/krisp-repl.sh b/bin/krisp-repl.sh new file mode 100755 index 0000000..2111447 --- /dev/null +++ b/bin/krisp-repl.sh @@ -0,0 +1,4 @@ +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +java -jar "${SCRIPT_DIR}/../build/libs/krisp-0.0.1-SNAPSHOT.jar" + diff --git a/build.gradle.kts b/build.gradle.kts index 9cd11b9..1fd72db 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } group = "com.ninjacontrol" -version = "1.0-SNAPSHOT" +version = "0.0.1-SNAPSHOT" repositories { mavenCentral() @@ -25,3 +25,9 @@ tasks.withType { from(zipTree(file.absoluteFile)).duplicatesStrategy = DuplicatesStrategy.EXCLUDE } } + +task("run-tests") { + dependsOn("testClasses") + mainClass.set("com.ninjacontrol.krisp.TestRunnerKt") + classpath = sourceSets["test"].runtimeClasspath +} diff --git a/src/main/kotlin/com/ninjacontrol/krisp/Main.kt b/src/main/kotlin/com/ninjacontrol/krisp/Main.kt index 88d8541..36272c9 100644 --- a/src/main/kotlin/com/ninjacontrol/krisp/Main.kt +++ b/src/main/kotlin/com/ninjacontrol/krisp/Main.kt @@ -41,7 +41,6 @@ fun evaluateFileAndExit(file: String) { fun start( file: String? = null, withInit: Boolean = true, - runTests: Boolean = false, args: Array? ) { args?.let { @@ -63,7 +62,6 @@ fun start( } when { file != null -> evaluateFileAndExit(file) - runTests -> Unit // TODO: start test: `runSuite()` else -> { banner() mainLoop() @@ -73,7 +71,8 @@ fun start( fun banner() { - val bannerExpression = """(println (str "Mal [" *host-language* "]"))""" + val versionString = "0.0.1" + val bannerExpression = "(println \"krisp v$versionString \")" try { rep(bannerExpression, replExecutionEnv) @@ -90,17 +89,15 @@ fun printHelp() { out("") out("options:") out("--skipInit\t\t\tDo not run init") - out("--runTests\t\t\tRun tests and quit") out("--help|-h\t\t\t\tPrint help and quit") } fun main(args: Array) { val file = args.getOrNull(0)?.let { if (it.startsWith("-")) null else it } val withInit = !args.contains("--skipInit") - val runTests = args.contains("--runTests") val printHelp = args.contains("--help") || args.contains("-h") when { printHelp -> printHelp() - else -> start(file, withInit, runTests, args) + else -> start(file, withInit, args) } } diff --git a/src/test/kotlin/com/ninjacontrol/krisp/AllTests.kt b/src/test/kotlin/com/ninjacontrol/krisp/AllTests.kt new file mode 100644 index 0000000..7c309be --- /dev/null +++ b/src/test/kotlin/com/ninjacontrol/krisp/AllTests.kt @@ -0,0 +1,32 @@ +package com.ninjacontrol.krisp + +class AllTests : TestSuite { + + override val name = "All tests" + + private val testSuites = listOf( + EnvironmentTest(), + NamespaceTest(), + StringTest(), + EvaluationTest(), + FunctionsTest(), + QuoteTest(), + MacroTest(), + MetadataTest() + ) + + override fun getTests() = testSuites.flatMap { testSuite -> testSuite.getTests() } + + override fun run(): Boolean { + val only = getTests().filter { it.only } + val suite = when { + only.isNotEmpty() -> listOf(CustomSuite(testCases = only)) + else -> testSuites + } + return suite.map { testSuite -> + log(testSuite.name) + testSuite.run() + } + .reduce { acc, result -> acc && result } + } +} diff --git a/src/test/kotlin/com/ninjacontrol/krisp/Assertions.kt b/src/test/kotlin/com/ninjacontrol/krisp/Assertions.kt new file mode 100644 index 0000000..9635e61 --- /dev/null +++ b/src/test/kotlin/com/ninjacontrol/krisp/Assertions.kt @@ -0,0 +1,79 @@ +package com.ninjacontrol.krisp + +class AssertionException(message: String, context: String? = null) : + Throwable(context?.let { "$context: $message" } ?: message) + +fun assertNeverExecuted() { + throw AssertionException("assertion failed, should never execute!") +} + +fun fail(message: String) { + throw AssertionException("test failed: $message") +} + +fun assertTrue(a: Boolean, context: String? = null) { + if (!a) throw AssertionException( + "assertion failed, value is false", + context + ) +} + +fun assertFalse(a: Boolean, context: String? = null) { + if (a) throw AssertionException( + "assertion failed, value is true", + context + ) +} + +fun assertNotNull(a: Any?, context: String? = null) { + if (a == null) throw AssertionException("assertion failed, value is null", context) +} + +fun assertNull(a: Any?, context: String? = null) { + if (a != null) throw AssertionException("assertion failed, value is not null", context) +} + +fun assertNil(a: MalType, context: String? = null) { + if (a !is MalNil) throw AssertionException("assertion failed, $a is not NIL", context) +} + +fun assertError(a: MalType, context: String? = null) { + if (a !is MalError) throw AssertionException("assertion failed, $a is not an error", context) +} + +fun assertNonError(a: MalType, context: String? = null) { + if (a is MalError) throw AssertionException("assertion failed, $a is an error", context) +} + +fun assertEqual(a: String, b: String, context: String? = null) { + if (a != b) throw AssertionException("assertion failed, \"$a\" is not equal to \"$b\"", context) +} + +fun assertEqual(a: Int, b: Int, context: String? = null) { + if (a != b) throw AssertionException("assertion failed, $a is not equal to $b", context) +} + +fun assertEqual(a: Char, b: Char, context: String? = null) { + if (a != b) throw AssertionException("assertion failed, '$a' is not equal to '$b'", context) +} + +fun assertEqual(a: MalType, b: MalType, context: String? = null) { + if (!isEqual(a, b)) throw AssertionException("assertion failed, $a is not equal to $b", context) +} + +fun assertReadEval( + input: String, + result: MalType, + env: Environment = replExecutionEnv, + context: String? = null +) { + val ast = re(input, env) + if (!isEqual( + ast, + result + ) + ) throw AssertionException( + "assertion failed, expected '$input' to result in $result, but was $ast instead", + context + ) +} diff --git a/src/test/kotlin/com/ninjacontrol/krisp/EnvironmentTest.kt b/src/test/kotlin/com/ninjacontrol/krisp/EnvironmentTest.kt new file mode 100644 index 0000000..e45df82 --- /dev/null +++ b/src/test/kotlin/com/ninjacontrol/krisp/EnvironmentTest.kt @@ -0,0 +1,164 @@ +package com.ninjacontrol.krisp + +class EnvironmentTest : TestSuite { + + override val name = "Environment" + + private val tests = listOf( + + test { + description = "Initialize environment" + verify = { + val env = Environment() + assertNotNull(env) + } + }, + test { + description = "Initialize environment with bindings" + verify = { + val env = Environment.withBindings( + outer = null, + bindings = listOf( + symbol("foo"), + symbol("bar"), + symbol("baz") + ), + expressions = listOf( + symbol("xux"), + symbol("bug"), + symbol("xug") + ) + ) + assertEqual(env.get(symbol("foo")), symbol("xux")) + assertEqual(env.get(symbol("bar")), symbol("bug")) + assertEqual(env.get(symbol("baz")), symbol("xug")) + } + }, + test { + description = + "Initialize environment with variadic parameters, 2 bindings, 3 expressions" + verify = { + val env = Environment.withBindings( + outer = null, + bindings = listOf( + symbol("foo"), + symbol("&"), + symbol("baz") + ), + expressions = listOf( + symbol("xux"), + symbol("bug"), + symbol("xug") + ) + ) + assertNotNull(env) + assertEqual(env.get(symbol("foo")), symbol("xux")) + assertEqual(env.get(symbol("baz")), list(symbol("bug"), symbol("xug"))) + } + }, + test { + description = + "Initialize environment with variadic parameters, 2 bindings, 2 expressions" + verify = { + val env = Environment.withBindings( + outer = null, + bindings = listOf( + symbol("foo"), + symbol("&"), + symbol("baz") + ), + expressions = listOf( + symbol("xux"), + symbol("xug") + ) + ) + assertNotNull(env) + assertEqual(env.get(symbol("foo")), symbol("xux")) + assertEqual(env.get(symbol("baz")), list(symbol("xug"))) + } + }, + test { + description = + "Initialize environment with variadic parameters, 5 bindings, 12 expressions" + verify = { + val env = Environment.withBindings( + outer = null, + bindings = listOf( + symbol("a"), + symbol("b"), + symbol("c"), + symbol("d"), + symbol("&"), + symbol("e"), + ), + expressions = listOf( + symbol("a1"), + symbol("b1"), + symbol("c1"), + symbol("d1"), + symbol("e1"), + symbol("e2"), + symbol("e3"), + symbol("e4"), + symbol("e5"), + symbol("e6"), + symbol("e7"), + symbol("e8"), + ) + ) + assertNotNull(env) + assertEqual(env.get(symbol("a")), symbol("a1")) + assertEqual(env.get(symbol("b")), symbol("b1")) + assertEqual(env.get(symbol("c")), symbol("c1")) + assertEqual(env.get(symbol("d")), symbol("d1")) + assertEqual( + env.get(symbol("e")), + list( + symbol("e1"), + symbol("e2"), + symbol("e3"), + symbol("e4"), + symbol("e5"), + symbol("e6"), + symbol("e7"), + symbol("e8"), + ) + ) + } + }, + test { + description = "Add symbol" + verify = { + val env = Environment() + val foo = symbol("foo") + val bar = symbol("bar") + env.set(foo, bar) + assertEqual(env.get(foo), bar) + } + }, + test { + description = "Find symbol in environment, symbol found" + verify = { + val env = Environment() + val foo = symbol("foo") + env.set(foo, foo) + env.find(foo) + assertTrue(env.find(foo) == env) + } + }, + test { + description = "Find symbol in environment, symbol not found" + verify = { + val env = Environment() + val foo = symbol("foo") + env.find(foo) + assertNull(env.find(foo)) + } + }, + + ) + + override fun getTests(): List = tests + override fun run(): Boolean = + verifyTests(tests) +} diff --git a/src/test/kotlin/com/ninjacontrol/krisp/EvaluationTest.kt b/src/test/kotlin/com/ninjacontrol/krisp/EvaluationTest.kt new file mode 100644 index 0000000..0408bb1 --- /dev/null +++ b/src/test/kotlin/com/ninjacontrol/krisp/EvaluationTest.kt @@ -0,0 +1,50 @@ +package com.ninjacontrol.krisp + +class EvaluationTest : TestSuite { + + override val name = "Evaluation and special forms" + + private val tests = listOf( + testReadEval { + description = "if: evaluates condition, true" + input = """(if true (+ 10 10) false)""" + expectedAst = int(20) + }, + + testReadEval { + description = "if: evaluates condition, false" + input = """(if false true (str "foobar"))""" + expectedAst = string("foobar") + }, + + testReadEval { + description = "let*: bind symbol" + input = """(let* (c 2) c)""" + expectedAst = int(2) + }, + testReadEval { + description = "let*: functions in bindings" + input = """(let* [foo (+ 20 13) bar (+ 2 foo)] (+ foo bar))""" + expectedAst = int(68) + }, + testReadEval { + description = "let*+do+def!: using outer environment" + input = """(do (def! X 24) (let* (Y 12) (let* (Z 39) X)))""" + expectedAst = int(24) + }, + testReadEvalThrows(NotFoundException("Symbol 'badSymbol' not found")) { + description = "do: catch error while evaluating" + input = """(do (+ 1 2) badSymbol true)""" + }, + testReadEval { + description = "def+let+fn: closures nests environments" + input = """(do (def! f (let* (x 10) (fn* () x))) (def! x 11) (f))""" + expectedAst = int(10) + } + + ) + + override fun getTests(): List = tests + override fun run(): Boolean = + verifyTests(tests) +} diff --git a/src/test/kotlin/com/ninjacontrol/krisp/FunctionsTest.kt b/src/test/kotlin/com/ninjacontrol/krisp/FunctionsTest.kt new file mode 100644 index 0000000..f408702 --- /dev/null +++ b/src/test/kotlin/com/ninjacontrol/krisp/FunctionsTest.kt @@ -0,0 +1,44 @@ +package com.ninjacontrol.krisp + +class FunctionsTest : TestSuite { + + override val name = "Functions" + + private val tests = listOf( + testReadEval { + description = "Invocation with simple parameters, return string" + input = """( (fn* (a) (pr-str "hello" a)) "world" )""" + expectedAst = string("\"hello\" \"world\"") + }, + testReadEval { + description = "Invocation with simple parameters, return integer" + input = """( (fn* (a b) (+ a b)) 10 12 )""" + expectedAst = int(22) + }, + testReadEval { + description = "Invocation with simple parameters, return list" + input = """( (fn* (a b) (list a b)) 10 12 )""" + expectedAst = list(int(10), int(12)) + }, + testReadEval { + description = "Invocation with variadic parameters, return list" + input = """( (fn* (a b c d & e) e) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 )""" + expectedAst = list( + int(5), + int(6), + int(7), + int(8), + int(9), + int(10), + int(11), + int(12), + int(13), + int(14) + ) + } + ) + + override fun getTests(): List = tests + override fun run(): Boolean = + verifyTests(tests) +} diff --git a/src/test/kotlin/com/ninjacontrol/krisp/MacroTest.kt b/src/test/kotlin/com/ninjacontrol/krisp/MacroTest.kt new file mode 100644 index 0000000..ff24a88 --- /dev/null +++ b/src/test/kotlin/com/ninjacontrol/krisp/MacroTest.kt @@ -0,0 +1,24 @@ +package com.ninjacontrol.krisp + +class MacroTest : TestSuite { + + override val name = "Macros" + + private val tests = listOf( + testReadEval { + description = "defmacro: define macro" + input = """(do (defmacro! leet (fn* () 1337)) (leet))""" + expectedAst = int(1337) + }, + testReadEval { + description = "macroexpand: expand macro" + input = """(do (defmacro! leet (fn* () 1337)) (macroexpand (leet)))""" + expectedAst = int(1337) + }, + + ) + + override fun getTests(): List = tests + override fun run(): Boolean = + verifyTests(tests) +} diff --git a/src/test/kotlin/com/ninjacontrol/krisp/MetadataTest.kt b/src/test/kotlin/com/ninjacontrol/krisp/MetadataTest.kt new file mode 100644 index 0000000..8f156f2 --- /dev/null +++ b/src/test/kotlin/com/ninjacontrol/krisp/MetadataTest.kt @@ -0,0 +1,34 @@ +package com.ninjacontrol.krisp + +class MetadataTest : TestSuite { + + override val name = "Metadata" + + private val tests = listOf( + testReadEval { + description = "with-meta+meta: set metadata on user function" + input = """(do (def! u (with-meta (fn* [x] (- 1 x)) "foo")) (meta u))""" + expectedAst = string("foo") + }, + testReadEval { + description = "with-meta+meta: set metadata on list" + input = """(do (def! u (with-meta (list 1 2 3) "foo")) (meta u))""" + expectedAst = string("foo") + }, + testReadEval { + description = "with-meta+meta: duplicate function" + input = """ + (do + (def! u (with-meta (fn* [x] (- 1 x)) "foo")) + (meta (with-meta u "bar")) + (meta u) + )""".trimMargin() + expectedAst = string("foo") + } + + ) + + override fun getTests(): List = tests + override fun run(): Boolean = + verifyTests(tests) +} diff --git a/src/test/kotlin/com/ninjacontrol/krisp/NamespaceTest.kt b/src/test/kotlin/com/ninjacontrol/krisp/NamespaceTest.kt new file mode 100644 index 0000000..e356ed6 --- /dev/null +++ b/src/test/kotlin/com/ninjacontrol/krisp/NamespaceTest.kt @@ -0,0 +1,222 @@ +package com.ninjacontrol.krisp + +class NamespaceTest : TestSuite { + + override val name = "Builtin functions in default namespace" + + private val tests = listOf( + testReadEval { + description = "list: no arguments returns empty list" + input = """(list)""" + expectedAst = com.ninjacontrol.krisp.emptyList() + }, + testReadEval { + description = "count: returns number of elements in list" + input = """(count (list 1 2 3 4 5 6 7 8 9))""" + expectedAst = int(9) + }, + testReadEval { + description = "count: empty list returns 0 elements" + input = """(count (list))""" + expectedAst = int(0) + }, + testReadEval { + description = "count: 'nil' counts as 0 elements" + input = """(count nil)""" + expectedAst = int(0) + }, + testReadEval { + description = "pr-str: 0 arguments returns empty string" + input = """(pr-str)""" + expectedAst = MalString("") + }, + testReadEval { + description = "read-string: eval string" + input = """(read-string "(1 2 3)")""" + expectedAst = list(int(1), int(2), int(3)) + }, + testReadEvalThrows(IOException("File \"this-does-not-exist\" does not exist")) { + description = "slurp: invalid filename throws error" + input = """(slurp "this-does-not-exist")""" + }, + testReadEval { + description = "atom: create atom" + input = """(do (def! a (atom 43)) a)""" + expectedAst = atom(int(43)) + }, + testReadEval { + description = "atom: deref atom" + input = """(do (def! a (atom 42)) (deref a))""" + expectedAst = int(42) + }, + testReadEval { + description = "atom: deref atom, shorthand" + input = """(do (def! a (atom 49)) @a)""" + expectedAst = int(49) + }, + testReadEval { + description = "atom: is atom?" + input = """(do (def! a (atom 49)) (atom? a))""" + expectedAst = True + }, + testReadEval { + description = "atom: reset value" + input = """(do (def! a (atom 49)) (reset! a 11))""" + expectedAst = int(11) + }, + testReadEval { + description = "atom: swap atom" + input = """(do (def! a (atom 49)) (swap! a (fn* [x y] (+ x y)) 10) @a)""" + expectedAst = int(59) + }, + testReadEval { + description = "read-string: read string w. newline" + input = """(read-string "\"\n\"")""" + expectedAst = string("\n") + }, + testReadEval { + description = "atom: closures retains atoms" + input = """(do (def! f (let* (a (atom 2)) (fn* () (deref a)))) (def! a (atom 3)) (f))""" + expectedAst = int(2) + }, + testReadEval { + description = "cons: returns list with new element prepended" + input = """(cons 0 '(1 2 3 4))""" + expectedAst = list(int(0), int(1), int(2), int(3), int(4)) + }, + testReadEval { + description = "cons: vector as second argument" + input = """(cons 0 [1 2 3 4])""" + expectedAst = list(int(0), int(1), int(2), int(3), int(4)) + }, + testReadEval { + description = "concat: returns concatenated list" + input = """(concat '(1 2) '(3 4) '(5 6) '(7 8))""" + expectedAst = list(int(1), int(2), int(3), int(4), int(5), int(6), int(7), int(8)) + }, + testReadEval { + description = "concat: vector parameter should return list" + input = """(concat [99 98])""" + expectedAst = list(int(99), int(98)) + }, + testReadEval { + description = "concat: vector + list + vector" + input = """(concat [99 98] (list 97 96) [95 94])""" + expectedAst = list(int(99), int(98), int(97), int(96), int(95), int(94)) + }, + testReadEval { + description = "try*/throw/catch*: throw and catch user exception" + input = """(try* (throw '(1 2 3)) (catch* E (do E)))""" + expectedAst = map( + key("type") to string("UserException"), + key("value") to list(int(1), int(2), int(3)) + ) + }, + testReadEval { + description = "try*/catch*: catch built-in exception" + input = """(try* unknown (catch* E (do E)))""" + expectedAst = map( + key("type") to string("NotFoundException"), + key("message") to string("Symbol 'unknown' not found") + ) + }, + testReadEval { + description = "apply: call function with argument list" + input = """(apply (fn* (l) (cons 10 l)) [[20 23]] )""" + expectedAst = list(int(10), int(20), int(23)) + }, + testReadEval { + description = "apply: call built function with argument list" + input = """(apply + [20 23])""" + expectedAst = int(43) + }, + testReadEval { + description = "apply: call function with concatenated argument list" + input = """(apply (fn* (& l) (cons 10 l)) 12 33 [20 23] )""" + expectedAst = list(int(10), int(12), int(33), int(20), int(23)) + }, + testReadEval { + description = "map: list" + input = """(map (fn* (o) (+ o 10)) [1 2 3 4 5])""" + expectedAst = list(int(11), int(12), int(13), int(14), int(15)) + }, + testReadEval { + description = "map: list, built-in function" + input = """(map list [1 2 3])""" + expectedAst = list(list(int(1)), list(int(2)), list(int(3))) + }, + testReadEval { + description = "true?+false?: predicates" + input = """(list (true? true) (false? false) (true? false) (false? true))""" + expectedAst = list(True, True, False, False) + }, + testReadEval { + description = "nil?: predicates" + input = """(list (nil? true) (nil? nil))""" + expectedAst = list(False, True) + }, + testReadEval { + description = "symbol?: predicates" + input = """(list (symbol? true) (symbol? 'apskaft) (symbol? 12) (symbol? "foo"))""" + expectedAst = list(False, True, False, False) + }, + testReadEval { + description = "hash-map: build map" + input = """(hash-map :foo 1 :bar 2 :baz 3)""" + expectedAst = map(key("foo") to int(1), key("bar") to int(2), key("baz") to int(3)) + }, + testReadEvalThrows(InvalidArgumentException("Expected an even number of arguments")) { + description = "hash-map: odd number of arguments throws exception" + input = """(hash-map :foo 1 :bar)""" + }, + testReadEval { + description = "assoc: add to map" + input = """(assoc {:foo 1 :bar 2} :baz 3)""" + expectedAst = map(key("foo") to int(1), key("bar") to int(2), key("baz") to int(3)) + }, + testReadEvalThrows(InvalidArgumentException("Expected an even number of arguments following the first argument")) { + description = "assoc: odd number of arguments throws exception" + input = """(assoc {:foo 1 :bar 2} :blurg)""" + }, + testReadEval { + description = "dissoc: remove from map" + input = """(dissoc {:foo 1 :bar 2 :baz 3} :baz :bar)""" + expectedAst = map(key("foo") to int(1)) + }, + testReadEval { + description = "get: get value from map by key" + input = """(get {:foo 1 :bar 2 :baz 3} :baz)""" + expectedAst = int(3) + }, + testReadEval { + description = "get: not found returns nil" + input = """(get {:foo 1 :bar 2 :baz 3} :ulon)""" + expectedAst = MalNil + }, + testReadEval { + description = "keys: list of keys" + input = """(keys {:foo 1 :bar 2 :baz 3})""" + expectedAst = list(key("foo"), key("bar"), key("baz")) + }, + testReadEval { + description = "vals: list of values" + input = """(vals {:foo 1 :bar 2 :baz 3})""" + expectedAst = list(int(1), int(2), int(3)) + }, + testReadEval { + description = "contains?: true if key exists" + input = """(contains? {:foo 1 :bar 2 :baz 3} :foo)""" + expectedAst = True + }, + testReadEval { + description = "contains?: false if key does not exist" + input = """(contains? {:foo 1 :bar 2 :baz 3} :uklan)""" + expectedAst = False + }, + + ) + + override fun getTests(): List = tests + override fun run(): Boolean = + verifyTests(tests) +} diff --git a/src/test/kotlin/com/ninjacontrol/krisp/QuoteTest.kt b/src/test/kotlin/com/ninjacontrol/krisp/QuoteTest.kt new file mode 100644 index 0000000..ebfb3ba --- /dev/null +++ b/src/test/kotlin/com/ninjacontrol/krisp/QuoteTest.kt @@ -0,0 +1,64 @@ +package com.ninjacontrol.krisp + +class QuoteTest : TestSuite { + + override val name = "Quoting" + + private val tests = listOf( + testReadEval { + description = "quote: return list" + input = """(quote (1 2 3))""" + expectedAst = list(int(1), int(2), int(3)) + }, + testReadEval { + description = "quote: shorthand" + input = """'(1 2 3)""" + expectedAst = list(int(1), int(2), int(3)) + }, + testReadEval { + description = "quote: as parameter" + input = """(str 'abc)""" + expectedAst = string("abc") + }, + testReadEval { + description = "quote: in `def!`" + input = """(do (def! u '(1 2 3)) u)""" + expectedAst = list(int(1), int(2), int(3)) + }, + testReadEval { + description = "quasiquote: quoted" + input = """(do (def! l '(i j)) (quasiquote (x l y)))""" + expectedAst = list(symbol("x"), symbol("l"), symbol("y")) + }, + testReadEval { + description = "quasiquote: unquoted" + input = """(do (def! ulon '(i j)) (quasiquote (x (unquote ulon) y)))""" + expectedAst = list(symbol("x"), list(symbol("i"), symbol("j")), symbol("y")) + }, + testReadEval { + description = "quasiquote: splice-unquoted" + input = """(do (def! ulon '(i j)) (quasiquote (x (splice-unquote ulon) y)))""" + expectedAst = list(symbol("x"), symbol("i"), symbol("j"), symbol("y")) + }, + testReadEval { + description = "quasiquote: splice-unquoted, shorthand" + input = """(do (def! ulon '(i j)) `(x ~@ulon y))""" + expectedAst = list(symbol("x"), symbol("i"), symbol("j"), symbol("y")) + }, + testReadEval { + description = "quasiquote: shorthand" + input = """`7""" + expectedAst = int(7) + }, + testReadEval { + description = "quasiquote: nested list" + input = """(quasiquote (1 2 (3 4)))""" + expectedAst = list(int(1), int(2), list(int(3), int(4))) + } + + ) + + override fun getTests(): List = tests + override fun run(): Boolean = + verifyTests(tests) +} diff --git a/src/test/kotlin/com/ninjacontrol/krisp/StringTest.kt b/src/test/kotlin/com/ninjacontrol/krisp/StringTest.kt new file mode 100644 index 0000000..5ed3198 --- /dev/null +++ b/src/test/kotlin/com/ninjacontrol/krisp/StringTest.kt @@ -0,0 +1,89 @@ +package com.ninjacontrol.krisp + +class StringTest : TestSuite { + + override val name = "Strings" + + private val tests = listOf( + + test { + description = "unescaping: transform strings w. escape sequences" + verify = { + val escaped = "foobar\\n\\r\\t" + val unescaped = unescape(escaped).first + assertNotNull(unescaped) + assertEqual(unescaped!!.length, 9) + assertEqual(unescaped[6], '\n') + assertEqual(unescaped[7], '\r') + assertEqual(unescaped[8], '\t') + } + }, + test { + description = "unescaping: reports invalid escape sequence & position" + verify = { + val escaped = "foobar\\n\\m\\t" + val (unescaped, error) = unescape(escaped) + val (errorMessage, charPos) = error!! + assertNull(unescaped) + assertNotNull(error) + assertEqual(errorMessage, "Invalid char.") + assertEqual(charPos!!, 9) + } + }, + test { + description = "escaping: produces an escaped string" + verify = { + val unescaped = "foobar\n\t\"" + val escaped = escape(unescaped) + assertEqual(escaped, "foobar\\n\\t\\\"") + } + }, + test { + description = "unquote: trim quotes" + verify = { + val quoted = "\"foobar\"" + val unquoted = "foobar" + val result = unquote(quoted) + assertNotNull(result) + assertEqual(result!!, unquoted) + assertEqual(unquote("\"\"")!!, "") + } + }, + test { + description = "quotes: verify quotes" + verify = { + val unbalancedError = + "String is unbalanced, first and last character must be a '\"'." + verifyQuotes(null)?.let { (error, pos) -> + assertEqual(error, "String is null.") + assertNull(pos) + } ?: fail("No error returned.") + verifyQuotes("")?.let { (error, pos) -> + assertEqual(error, unbalancedError) + assertNull(pos) + } ?: fail("No error returned.") + verifyQuotes("\"foobar")?.let { (error, pos) -> + assertEqual(error, unbalancedError) + assertEqual(pos!!, 6) + } ?: fail("No error returned.") + verifyQuotes("\"foobar\\\"")?.let { (error, pos) -> + assertEqual(error, unbalancedError) + assertEqual(pos!!, 8) + } ?: fail("No error returned.") + verifyQuotes("\"dpp\"djklj\"")?.let { (error, pos) -> + assertEqual(error, "Unexpected end of string.") + assertEqual(pos!!, 4) + } ?: fail("No error returned.") + + assertNull(verifyQuotes("\"foo\"")) + assertNull(verifyQuotes("\"fo\\\"o\"")) + assertNull(verifyQuotes("\"foo=\\\"bar\\\"\"")) + } + } + + ) + + override fun getTests(): List = tests + override fun run(): Boolean = + verifyTests(tests) +} diff --git a/src/test/kotlin/com/ninjacontrol/krisp/TestRunner.kt b/src/test/kotlin/com/ninjacontrol/krisp/TestRunner.kt new file mode 100644 index 0000000..673e79d --- /dev/null +++ b/src/test/kotlin/com/ninjacontrol/krisp/TestRunner.kt @@ -0,0 +1,29 @@ +package com.ninjacontrol.krisp + +import kotlin.system.exitProcess + +fun main() { + val tests = AllTests() + runSuite(tests) +} + +fun runSuite(tests: TestSuite) { + var result = false + try { + result = tests.run() + } catch (e: Throwable) { + result = false + tests.log("*** Got exception ${e.javaClass}, '${e.message}'") + } finally { + tests.log("--------------------") + tests.log("Test finished: ", newline = false) + when (result) { + true -> { + tests.log(tests.passText); exitProcess(0) + } + false -> { + tests.log(tests.failText); exitProcess(1) + } + } + } +} diff --git a/src/test/kotlin/com/ninjacontrol/krisp/TestSuite.kt b/src/test/kotlin/com/ninjacontrol/krisp/TestSuite.kt new file mode 100644 index 0000000..9988ef2 --- /dev/null +++ b/src/test/kotlin/com/ninjacontrol/krisp/TestSuite.kt @@ -0,0 +1,90 @@ +package com.ninjacontrol.krisp + +interface TestSuite { + + val name: String + + val pass: String + get() = "[✅]" + val fail: String + get() = "[❌]" + val passText: String + get() = "PASS" + val failText: String + get() = "FAIL" + + fun log(message: String, newline: Boolean = true) { + if (newline) println(message) else print(message) + } + + fun getTests(): List + fun verifyTests(testCases: List): Boolean { + return testCases.map { testCase -> + val context = testCase.description ?: "(no description)" + try { + + testCase.verify() + log("$pass $context") + true + } catch (e: AssertionException) { + e.message?.let { log("$fail $context: $it") } ?: log("$fail $context") + false + } + }.reduce { acc, result -> acc && result } + } + + fun run(): Boolean +} + +abstract class TestCase(var description: String? = null) { + var verify: () -> Unit = { assertNeverExecuted() } + var only = false +} + +class ReadEvalTestCase : TestCase() { + lateinit var input: String + lateinit var expectedAst: MalType +} + +class DefaultTestCase : TestCase() + +class CustomSuite(private val testCases: List) : TestSuite { + override val name = "Custom" + override fun getTests(): List = testCases + override fun run(): Boolean = verifyTests(testCases) +} + +fun test(case: DefaultTestCase.() -> Unit): TestCase { + val t = DefaultTestCase() + t.case() + return t +} + +fun testReadEval(case: ReadEvalTestCase.() -> Unit): TestCase { + val t = ReadEvalTestCase() + t.case() + t.verify = { assertReadEval(input = t.input, result = t.expectedAst) } + return t +} + +inline fun testReadEvalThrows( + exception: T, + crossinline case: ReadEvalTestCase.() -> Unit +): TestCase { + val t = ReadEvalTestCase() + var caught: Throwable? = null + t.case() + t.verify = { + try { + re(t.input, replExecutionEnv) + } catch (e: Throwable) { + caught = e + } + when { + caught == null -> throw AssertionException("No exception thrown") + caught !is T -> throw AssertionException("Unexpected exception, $caught") + (caught as T).message != exception.message -> throw AssertionException("Expected exception with message '${exception.message}' but got '${(caught as T).message}' instead") + } + } + return t +}