From 844b6ab6e8bc5ddf07fee5efb675c735540af803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 16 Aug 2024 16:26:46 +0200 Subject: [PATCH 01/14] Make the Effekt testing suite a little prettier --- libraries/common/string.effekt | 51 +++++++----- libraries/common/test.effekt | 137 +++++++++++++++++++++++++-------- 2 files changed, 137 insertions(+), 51 deletions(-) diff --git a/libraries/common/string.effekt b/libraries/common/string.effekt index c43cb6281..a4f42c638 100644 --- a/libraries/common/string.effekt +++ b/libraries/common/string.effekt @@ -245,25 +245,38 @@ def printing[T] { prog: => T / Stream }: T = <> // ANSI escape codes namespace ANSI { - val BLACK = "\u001b[30m" - val RED = "\u001b[31m" - val GREEN = "\u001b[32m" - val YELLOW = "\u001b[33m" - val BLUE = "\u001b[34m" - val MAGENTA = "\u001b[35m" - val CYAN = "\u001b[36m" - val WHITE = "\u001b[37m" - - val BG_BLACK = "\u001b[40m" - val BG_RED = "\u001b[41m" - val BG_GREEN = "\u001b[42m" - val BG_YELLOW = "\u001b[43m" - val BG_BLUE = "\u001b[44m" - val BG_MAGENTA = "\u001b[45m" - val BG_CYAN = "\u001b[46m" - val BG_WHITE = "\u001b[47m" - - val RESET = "\u001b[0m" + val CSI = "\u001b[" + + def escape(s: String) = CSI ++ s ++ "m" + + val BLACK = escape("30") + val RED = escape("31") + val GREEN = escape("32") + val YELLOW = escape("33") + val BLUE = escape("34") + val MAGENTA = escape("35") + val CYAN = escape("36") + val WHITE = escape("37") + + val BG_BLACK = escape("40") + val BG_RED = escape("41") + val BG_GREEN = escape("42") + val BG_YELLOW = escape("43") + val BG_BLUE = escape("44") + val BG_MAGENTA = escape("45") + val BG_CYAN = escape("46") + val BG_WHITE = escape("47") + + val RESET = escape("0") + + val BOLD = escape("1") + val FAINT = escape("2") + val ITALIC = escape("3") + val UNDERLINE = escape("4") + val BLINK = escape("5") + val REVERSE = escape("7") + val CROSSOUT = escape("9") + val OVERLINE = escape("53") } diff --git a/libraries/common/test.effekt b/libraries/common/test.effekt index f52323d2b..3395ed3ee 100644 --- a/libraries/common/test.effekt +++ b/libraries/common/test.effekt @@ -1,5 +1,55 @@ +<<<<<<< HEAD import string import process +||||||| parent of e3e9b93b (Make the Effekt testing suite a little prettier) +import string +======= +import bench + +interface Formatted { + def supportsEscape(escape: String): Bool +} + +namespace Formatted { + def formatting[R] { prog : => R / Formatted }: R = + try { + prog() + } with Formatted { + def supportsEscape(escape: String) = resume(true) + } + + def noFormatting[R] { prog : => R / Formatted }: R = + try { + prog() + } with Formatted { + def supportsEscape(escape: String) = resume(false) + } + + def tryEmit(escape: String): String / Formatted = + if (do supportsEscape(escape)) { escape } else { "" } +} + +namespace Duration { + def diff(from: Int, to: Int) = to - from + def now(): Int = bench::relativeTimestamp() + + def format(nanos: Int): String = { + val micros = nanos / 1000000 + val sub = (nanos.mod(1000000).toDouble / 10000.0).round + + if (sub == 100) { + (micros + 1).show ++ ".0ms" + } else { + micros.show ++ "." ++ sub.show ++ "ms" + } + } +} + +def red(s: String) = Formatted::tryEmit(ANSI::RED) ++ s ++ Formatted::tryEmit(ANSI::RESET) +def green(s: String) = Formatted::tryEmit(ANSI::GREEN) ++ s ++ Formatted::tryEmit(ANSI::RESET) +def dim(s: String) = Formatted::tryEmit(ANSI::FAINT) ++ s ++ Formatted::tryEmit(ANSI::RESET) +def bold(s: String) = Formatted::tryEmit(ANSI::BOLD) ++ s ++ Formatted::tryEmit(ANSI::RESET) +>>>>>>> e3e9b93b (Make the Effekt testing suite a little prettier) interface Assertion { def assert(condition: Bool, msg: String): Unit @@ -29,65 +79,87 @@ def assert(obtained: Char, expected: Char, msg: String): Unit / Assertion = def assert(obtained: String, expected: String, msg: String): Unit / Assertion = assertEqual(obtained, expected, msg) { (x, y) => x == y } -def assert(obtained: Int, expected: Int): Unit / Assertion = +def assert(obtained: Int, expected: Int): Unit / { Assertion, Formatted } = assertEqual(obtained, expected) { (x, y) => x == y } { x => show(x) } -def assert(obtained: Bool, expected: Bool): Unit / Assertion = +def assert(obtained: Bool, expected: Bool): Unit / { Assertion, Formatted } = assertEqual(obtained, expected) { (x, y) => x == y } { x => show(x) } -def assert(obtained: Char, expected: Char): Unit / Assertion = +def assert(obtained: Char, expected: Char): Unit / { Assertion, Formatted } = assertEqual(obtained, expected) { (x, y) => x == y } { x => show(x) } -def assert(obtained: String, expected: String): Unit / Assertion = +def assert(obtained: String, expected: String): Unit / { Assertion, Formatted } = assertEqual(obtained, expected) { (x, y) => x == y } { x => show(x) } def assertEqual[A](obtained: A, expected: A, msg: String): Unit / Assertion = assertEqual(obtained, expected, msg) { (x, y) => x.equals(y) } -def assertEqual[A](obtained: A, expected: A): Unit / Assertion = +def assertEqual[A](obtained: A, expected: A): Unit / { Assertion, Formatted } = assertEqual(obtained, expected) { (x, y) => x.equals(y) } { x => x.genericShow } def assertEqual[A](obtained: A, expected: A, msg: String) { equals: (A, A) => Bool }: Unit / Assertion = do assert(equals(obtained, expected), msg) -def assertEqual[A](obtained: A, expected: A) { equals: (A, A) => Bool } { show: A => String }: Unit / Assertion = - do assert(equals(obtained, expected), "Obtained: " ++ show(obtained) ++ "\n but expected:" ++ show(expected)) - +def assertEqual[A](obtained: A, expected: A) { equals: (A, A) => Bool } { show: A => String }: Unit / { Assertion, Formatted } = + do assert(equals(obtained, expected), Formatted::tryEmit(ANSI::RESET) ++ "Expected: ".dim ++ show(expected).green ++ "\n Obtained: ".dim ++ show(obtained).red) + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // Here's an accidental capture! Can we prevent this somehow nicely? interface Test { - def success(name: String): Unit - def failure(name: String, msg: String): Unit + def success(name: String, duration: Int): Unit + def failure(name: String, msg: String, duration: Int): Unit } -def test(name: String) { body: => Unit / Assertion } = - try { body(); do success(name) } with Assertion { - def assert(condition, msg) = if (condition) resume(()) else do failure(name, msg) +def test(name: String) { body: => Unit / Assertion } = { + val startTime = Duration::now() + try { + body() + val duration = Duration::diff(startTime, Duration::now()) + do success(name, duration) + } with Assertion { + def assert(condition, msg) = + if (condition) resume(()) + else { + val duration = Duration::diff(startTime, Duration::now()) + do failure(name, msg, duration) + } } +} + +def suite(name: String) { body: => Unit / { Test, Formatted } }: Bool / {} = { + with Formatted::formatting; -def suite(name: String) { body: => Unit / Test }: Bool = { - println("Running suite: " ++ name); + println(name.bold) var failed = 0 var passed = 0 - try { body() } with Test { - def success(name) = { - passed = passed + 1 - println(ANSI::GREEN ++ "+ " ++ name ++ "" ++ ANSI::RESET); - resume(()) + val totalDuration = timed { + try { body() } with Test { + def success(name, duration) = { + passed = passed + 1 + println("✓".green ++ " " ++ name ++ " " ++ ("[" ++ Duration::format(duration) ++ "]").dim) + resume(()) + } + def failure(name, msg, duration) = { + failed = failed + 1 + println("✕".red ++ " " ++ name ++ " " ++ ("[" ++ Duration::format(duration) ++ "]").dim) + println(" " ++ msg.red) + resume(()) + } } - def failure(name, msg) = { - failed = failed + 1 - println(ANSI::RED ++ "- (FAIL) " ++ name ++ "\n " ++ msg ++ ANSI::RESET); - resume(()) - } - } - if (failed > 0) { - println(ANSI::RED ++ "Some tests failed (" ++ passed.show ++ " passed, " ++ failed.show ++ " failed)" ++ ANSI::RESET) - false - } else { - println(ANSI::GREEN ++ "All tests passed (" ++ passed.show ++ " passed)" ++ ANSI::RESET) - true } + + def color(s: String, n: Int) { colorIfNonZero: String => String / Formatted } = + if (n == 0) { dim(s) } + else { colorIfNonZero(s) } + + println("") + println(" " ++ (passed.show ++ " pass").color(passed) { green }) + println(" " ++ (failed.show ++ " fail").color(failed) { red }) + println(" " ++ (passed + failed).show ++ " tests total" ++ " " ++ ("[" ++ Duration::format(totalDuration) ++ "]").dim) + println("") + + return failed == 0 } /// Use as `def main() = mainSuite("...") { ... }` @@ -96,3 +168,4 @@ def mainSuite(name: String) { body: => Unit / Test }: Unit = { val exitCode = if (result) 0 else 1 exit(exitCode) } + From 851f5dffda831218f927bb73cd815e4f5e03f659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 16 Aug 2024 17:59:54 +0200 Subject: [PATCH 02/14] Try to fix tests, add a version without bench --- examples/pos/lib_test.check | 15 +++++++++------ examples/pos/lib_test.effekt | 3 ++- examples/stdlib/test/test.check | 13 ++++++++----- examples/stdlib/test/test.effekt | 3 ++- libraries/common/test.effekt | 21 ++++++++++++++++----- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/examples/pos/lib_test.check b/examples/pos/lib_test.check index 6d5e34729..a9d5a82b1 100644 --- a/examples/pos/lib_test.check +++ b/examples/pos/lib_test.check @@ -1,6 +1,9 @@ -Running suite: Arithmetic -+ addition -- (FAIL) faulty addition - 2 + 2 should be 4! -+ multiplication -Some tests failed (2 passed, 1 failed) +Arithmetic +✓ addition +✕ faulty addition + 2 + 2 should be 4! +✓ multiplication + + 2 pass + 1 fail + 3 tests total diff --git a/examples/pos/lib_test.effekt b/examples/pos/lib_test.effekt index 3c32f3f82..624680737 100644 --- a/examples/pos/lib_test.effekt +++ b/examples/pos/lib_test.effekt @@ -2,7 +2,8 @@ import test def main() = { - suite("Arithmetic") { + // Don't print out times in CI. + suite("Arithmetic", false) { test("addition") { val x = 1; diff --git a/examples/stdlib/test/test.check b/examples/stdlib/test/test.check index 3a639652a..ea4b0b33a 100644 --- a/examples/stdlib/test/test.check +++ b/examples/stdlib/test/test.check @@ -1,5 +1,8 @@ -Running suite: Test test suite -+ Test test assertTrue -+ Test test assertFalse -+ Test test assert -All tests passed (3 passed) \ No newline at end of file +Test test suite +✓ Test test assertTrue +✓ Test test assertFalse +✓ Test test assert + + 3 pass + 0 fail + 3 tests total diff --git a/examples/stdlib/test/test.effekt b/examples/stdlib/test/test.effekt index 140a0d668..beeb793e0 100644 --- a/examples/stdlib/test/test.effekt +++ b/examples/stdlib/test/test.effekt @@ -1,7 +1,8 @@ import test def main() = { - suite("Test test suite") { + // Don't print out times in CI. + suite("Test test suite", false) { test("Test test assertTrue") { assertTrue(true) assertTrue(true, "true") diff --git a/libraries/common/test.effekt b/libraries/common/test.effekt index 3395ed3ee..7dce217a3 100644 --- a/libraries/common/test.effekt +++ b/libraries/common/test.effekt @@ -126,9 +126,19 @@ def test(name: String) { body: => Unit / Assertion } = { } } -def suite(name: String) { body: => Unit / { Test, Formatted } }: Bool / {} = { +/// Run a test suite with a given `name`. +/// If `printTimes` is `true` (or missing), prints out time in milliseconds. +/// Formats automatically using ANSI escapes. +def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } }: Bool / {} = { with Formatted::formatting; + def ms(duration: Int): String / Formatted = + if (printTimes) { + ("[" ++ Duration::format(duration) ++ "]").dim + } else { + "" + } + println(name.bold) var failed = 0 var passed = 0 @@ -137,12 +147,12 @@ def suite(name: String) { body: => Unit / { Test, Formatted } }: Bool / {} = { try { body() } with Test { def success(name, duration) = { passed = passed + 1 - println("✓".green ++ " " ++ name ++ " " ++ ("[" ++ Duration::format(duration) ++ "]").dim) + println("✓".green ++ " " ++ name ++ " " ++ duration.ms) resume(()) } def failure(name, msg, duration) = { failed = failed + 1 - println("✕".red ++ " " ++ name ++ " " ++ ("[" ++ Duration::format(duration) ++ "]").dim) + println("✕".red ++ " " ++ name ++ " " ++ duration.ms) println(" " ++ msg.red) resume(()) } @@ -156,8 +166,7 @@ def suite(name: String) { body: => Unit / { Test, Formatted } }: Bool / {} = { println("") println(" " ++ (passed.show ++ " pass").color(passed) { green }) println(" " ++ (failed.show ++ " fail").color(failed) { red }) - println(" " ++ (passed + failed).show ++ " tests total" ++ " " ++ ("[" ++ Duration::format(totalDuration) ++ "]").dim) - println("") + println(" " ++ (passed + failed).show ++ " tests total" ++ " " ++ totalDuration.ms) return failed == 0 } @@ -169,3 +178,5 @@ def mainSuite(name: String) { body: => Unit / Test }: Unit = { exit(exitCode) } +def suite(name: String) { body: => Unit / { Test, Formatted } }: Bool / {} = + suite(name, true) { body } From c18ed9f84f5050ff61833356bc95fd597649f7ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 16 Aug 2024 19:37:31 +0200 Subject: [PATCH 03/14] Reduce the 'Duration' submodule --- libraries/common/bench.effekt | 15 +++++++++++++++ libraries/common/test.effekt | 22 +++++----------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/libraries/common/bench.effekt b/libraries/common/bench.effekt index cd31273db..2b55303e4 100644 --- a/libraries/common/bench.effekt +++ b/libraries/common/bench.effekt @@ -58,3 +58,18 @@ def measure(warmup: Int, iterations: Int) { block: => Unit }: Unit = { run(warmup, false) run(iterations, true) } + +/** + * Takes a duration in nanoseconds and formats it in milliseconds with precision of two decimal digits. + */ +def formatMs(nanos: Int): String = { + val micros = nanos / 1000000 + val sub = (nanos.mod(1000000).toDouble / 10000.0).round + + if (sub == 100) { + // overflow because of rounding + (micros + 1).show ++ ".0ms" + } else { + micros.show ++ "." ++ sub.show ++ "ms" + } +} diff --git a/libraries/common/test.effekt b/libraries/common/test.effekt index 7dce217a3..8af774767 100644 --- a/libraries/common/test.effekt +++ b/libraries/common/test.effekt @@ -30,19 +30,7 @@ namespace Formatted { } namespace Duration { - def diff(from: Int, to: Int) = to - from - def now(): Int = bench::relativeTimestamp() - - def format(nanos: Int): String = { - val micros = nanos / 1000000 - val sub = (nanos.mod(1000000).toDouble / 10000.0).round - - if (sub == 100) { - (micros + 1).show ++ ".0ms" - } else { - micros.show ++ "." ++ sub.show ++ "ms" - } - } + def diff(fromNanos: Int, toNanos: Int) = toNanos - fromNanos } def red(s: String) = Formatted::tryEmit(ANSI::RED) ++ s ++ Formatted::tryEmit(ANSI::RESET) @@ -111,16 +99,16 @@ interface Test { } def test(name: String) { body: => Unit / Assertion } = { - val startTime = Duration::now() + val startTime = bench::relativeTimestamp() try { body() - val duration = Duration::diff(startTime, Duration::now()) + val duration = Duration::diff(startTime, bench::relativeTimestamp()) do success(name, duration) } with Assertion { def assert(condition, msg) = if (condition) resume(()) else { - val duration = Duration::diff(startTime, Duration::now()) + val duration = Duration::diff(startTime, bench::relativeTimestamp()) do failure(name, msg, duration) } } @@ -134,7 +122,7 @@ def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } def ms(duration: Int): String / Formatted = if (printTimes) { - ("[" ++ Duration::format(duration) ++ "]").dim + ("[" ++ bench::formatMs(duration) ++ "]").dim } else { "" } From 45aafd396beb4264a7be6608fcfc6aef060e72b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 16 Aug 2024 19:44:13 +0200 Subject: [PATCH 04/14] Fix formatting, rename 'dimWhenZeroElse' (sigh) --- libraries/common/test.effekt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/common/test.effekt b/libraries/common/test.effekt index 8af774767..31f253345 100644 --- a/libraries/common/test.effekt +++ b/libraries/common/test.effekt @@ -90,7 +90,7 @@ def assertEqual[A](obtained: A, expected: A, msg: String) { equals: (A, A) => Bo def assertEqual[A](obtained: A, expected: A) { equals: (A, A) => Bool } { show: A => String }: Unit / { Assertion, Formatted } = do assert(equals(obtained, expected), Formatted::tryEmit(ANSI::RESET) ++ "Expected: ".dim ++ show(expected).green ++ "\n Obtained: ".dim ++ show(obtained).red) - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // NOTE: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // Here's an accidental capture! Can we prevent this somehow nicely? interface Test { @@ -122,7 +122,7 @@ def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } def ms(duration: Int): String / Formatted = if (printTimes) { - ("[" ++ bench::formatMs(duration) ++ "]").dim + " " ++ ("[" ++ bench::formatMs(duration) ++ "]").dim } else { "" } @@ -135,26 +135,26 @@ def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } try { body() } with Test { def success(name, duration) = { passed = passed + 1 - println("✓".green ++ " " ++ name ++ " " ++ duration.ms) + println("✓".green ++ " " ++ name ++ duration.ms) resume(()) } def failure(name, msg, duration) = { failed = failed + 1 - println("✕".red ++ " " ++ name ++ " " ++ duration.ms) + println("✕".red ++ " " ++ name ++ duration.ms) println(" " ++ msg.red) resume(()) } } } - def color(s: String, n: Int) { colorIfNonZero: String => String / Formatted } = + def dimWhenZeroElse(s: String, n: Int) { colorIfNonZero: String => String / Formatted } = if (n == 0) { dim(s) } else { colorIfNonZero(s) } println("") - println(" " ++ (passed.show ++ " pass").color(passed) { green }) - println(" " ++ (failed.show ++ " fail").color(failed) { red }) - println(" " ++ (passed + failed).show ++ " tests total" ++ " " ++ totalDuration.ms) + println(" " ++ (passed.show ++ " pass").dimWhenZeroElse(passed) { green }) + println(" " ++ (failed.show ++ " fail").dimWhenZeroElse(failed) { red }) + println(" " ++ (passed + failed).show ++ " tests total" ++ totalDuration.ms) return failed == 0 } From f54bcad607a340d15b0cc7fd55e45c548dec0de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Mon, 26 Aug 2024 11:01:28 +0200 Subject: [PATCH 05/14] Try importing performance explicitly in NodeJS --- libraries/common/bench.effekt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/common/bench.effekt b/libraries/common/bench.effekt index 2b55303e4..17ac3a6d4 100644 --- a/libraries/common/bench.effekt +++ b/libraries/common/bench.effekt @@ -27,6 +27,11 @@ extern llvm """ declare i32 @clock_gettime(i32, ptr) """ +// This will not be needed from NodeJS 16 and newer. +extern jsNode """ + const { performance } = require('perf_hooks'); +""" + /** * High-precision timestamp in nanoseconds that should be for measurements. * From 671d9d9816c48c0d1b0bde7114e47642dc83fcce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 30 Aug 2024 14:30:52 +0200 Subject: [PATCH 06/14] Shuffle test results around --- examples/{pos/lib_test.check => stdlib/test/fail.check} | 0 examples/{pos/lib_test.effekt => stdlib/test/fail.effekt} | 0 examples/stdlib/test/{test.check => pass.check} | 0 examples/stdlib/test/{test.effekt => pass.effekt} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename examples/{pos/lib_test.check => stdlib/test/fail.check} (100%) rename examples/{pos/lib_test.effekt => stdlib/test/fail.effekt} (100%) rename examples/stdlib/test/{test.check => pass.check} (100%) rename examples/stdlib/test/{test.effekt => pass.effekt} (100%) diff --git a/examples/pos/lib_test.check b/examples/stdlib/test/fail.check similarity index 100% rename from examples/pos/lib_test.check rename to examples/stdlib/test/fail.check diff --git a/examples/pos/lib_test.effekt b/examples/stdlib/test/fail.effekt similarity index 100% rename from examples/pos/lib_test.effekt rename to examples/stdlib/test/fail.effekt diff --git a/examples/stdlib/test/test.check b/examples/stdlib/test/pass.check similarity index 100% rename from examples/stdlib/test/test.check rename to examples/stdlib/test/pass.check diff --git a/examples/stdlib/test/test.effekt b/examples/stdlib/test/pass.effekt similarity index 100% rename from examples/stdlib/test/test.effekt rename to examples/stdlib/test/pass.effekt From 761d01d9ee964952d9f31867c70eff6492c2ae76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 30 Aug 2024 14:46:12 +0200 Subject: [PATCH 07/14] Don't test the test library on LLVM and MLton --- effekt/jvm/src/test/scala/effekt/StdlibTests.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala index a58c7d8a4..9876bf1c1 100644 --- a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala +++ b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala @@ -45,9 +45,12 @@ class StdlibMLTests extends StdlibTests { // unicode is not supported in the ML backend examplesDir / "stdlib" / "string" / "unicode.effekt", + // missing support for multibyte character escape in a string + examplesDir / "stdlib" / "test", + // Not implemented yet examplesDir / "stdlib" / "bytes", - examplesDir / "stdlib" / "io" + examplesDir / "stdlib" / "io", ) } class StdlibLLVMTests extends StdlibTests { @@ -77,5 +80,8 @@ class StdlibLLVMTests extends StdlibTests { examplesDir / "stdlib" / "list" / "build.effekt", examplesDir / "stdlib" / "string" / "strings.effekt", examplesDir / "stdlib" / "string" / "unicode.effekt", + + // missing support for top-level constants for ANSI escapes + examplesDir / "stdlib" / "test", ) } From 936ca8fb477208ebd7f7425d3ac71ab7a2963c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 30 Aug 2024 15:16:58 +0200 Subject: [PATCH 08/14] Add a few more comments in 'test' --- libraries/common/test.effekt | 58 +++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/libraries/common/test.effekt b/libraries/common/test.effekt index 31f253345..851756ee1 100644 --- a/libraries/common/test.effekt +++ b/libraries/common/test.effekt @@ -1,9 +1,5 @@ -<<<<<<< HEAD import string import process -||||||| parent of e3e9b93b (Make the Effekt testing suite a little prettier) -import string -======= import bench interface Formatted { @@ -11,33 +7,33 @@ interface Formatted { } namespace Formatted { + /// Run given block of code, allowing all formatting def formatting[R] { prog : => R / Formatted }: R = - try { - prog() - } with Formatted { + try { prog() } with Formatted { def supportsEscape(escape: String) = resume(true) } + /// Run given block of code, ignoring all formatting def noFormatting[R] { prog : => R / Formatted }: R = - try { - prog() - } with Formatted { + try { prog() } with Formatted { def supportsEscape(escape: String) = resume(false) } def tryEmit(escape: String): String / Formatted = - if (do supportsEscape(escape)) { escape } else { "" } + if (do supportsEscape(escape)) escape else "" + + def withEscape(text: String, colorEscape: String): String / Formatted = + tryEmit(colorEscape) ++ s ++ tryEmit(ANSI::RESET) } namespace Duration { def diff(fromNanos: Int, toNanos: Int) = toNanos - fromNanos } -def red(s: String) = Formatted::tryEmit(ANSI::RED) ++ s ++ Formatted::tryEmit(ANSI::RESET) -def green(s: String) = Formatted::tryEmit(ANSI::GREEN) ++ s ++ Formatted::tryEmit(ANSI::RESET) -def dim(s: String) = Formatted::tryEmit(ANSI::FAINT) ++ s ++ Formatted::tryEmit(ANSI::RESET) -def bold(s: String) = Formatted::tryEmit(ANSI::BOLD) ++ s ++ Formatted::tryEmit(ANSI::RESET) ->>>>>>> e3e9b93b (Make the Effekt testing suite a little prettier) +def red(s: String) = s.withEscape(ANSI::RED) +def green(s: String) = s.withEscape(ANSI::GREEN) +def dim(s: String) = s.withEscape(ANSI::FAINT) +def bold(s: String) = s.withEscape(ANSI::BOLD) interface Assertion { def assert(condition: Bool, msg: String): Unit @@ -93,11 +89,14 @@ def assertEqual[A](obtained: A, expected: A) { equals: (A, A) => Bool } { show: // NOTE: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // Here's an accidental capture! Can we prevent this somehow nicely? + interface Test { def success(name: String, duration: Int): Unit def failure(name: String, msg: String, duration: Int): Unit } +/// Runs the `body` as a test under the given `name` +/// See `suite` for examples. def test(name: String) { body: => Unit / Assertion } = { val startTime = bench::relativeTimestamp() try { @@ -117,6 +116,15 @@ def test(name: String) { body: => Unit / Assertion } = { /// Run a test suite with a given `name`. /// If `printTimes` is `true` (or missing), prints out time in milliseconds. /// Formats automatically using ANSI escapes. +/// +/// Example: +/// ```effekt +/// suite("My Tests") { +/// test("1 + 1 == 2") { +/// assertEqual(1 + 1, 2) +/// } +/// } +/// ``` def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } }: Bool / {} = { with Formatted::formatting; @@ -127,17 +135,27 @@ def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } "" } - println(name.bold) + def dimWhenZeroElse(s: String, n: Int) { colorIfNonZero: String => String / Formatted } = + if (n == 0) { dim(s) } + else { colorIfNonZero(s) } + var failed = 0 var passed = 0 + // 1) Print the name of the test + println(name.bold) + + // 2) Run the tests, timing them val totalDuration = timed { try { body() } with Test { + // 2a) Handle a passing test on success def success(name, duration) = { passed = passed + 1 println("✓".green ++ " " ++ name ++ duration.ms) resume(()) } + + // 2b) Handle a failing test on failure, additionally printing its message def failure(name, msg, duration) = { failed = failed + 1 println("✕".red ++ " " ++ name ++ duration.ms) @@ -147,15 +165,13 @@ def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } } } - def dimWhenZeroElse(s: String, n: Int) { colorIfNonZero: String => String / Formatted } = - if (n == 0) { dim(s) } - else { colorIfNonZero(s) } - + // 3) Format the test results println("") println(" " ++ (passed.show ++ " pass").dimWhenZeroElse(passed) { green }) println(" " ++ (failed.show ++ " fail").dimWhenZeroElse(failed) { red }) println(" " ++ (passed + failed).show ++ " tests total" ++ totalDuration.ms) + // 4) Return true if all tests succeeded, otherwise false return failed == 0 } From b4dadfab25cc7d3c75513235e68aae3d69431e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 30 Aug 2024 15:31:42 +0200 Subject: [PATCH 09/14] Rename parameters of formatting functions --- libraries/common/test.effekt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/common/test.effekt b/libraries/common/test.effekt index 851756ee1..c05edca1b 100644 --- a/libraries/common/test.effekt +++ b/libraries/common/test.effekt @@ -23,17 +23,17 @@ namespace Formatted { if (do supportsEscape(escape)) escape else "" def withEscape(text: String, colorEscape: String): String / Formatted = - tryEmit(colorEscape) ++ s ++ tryEmit(ANSI::RESET) + tryEmit(colorEscape) ++ text ++ tryEmit(ANSI::RESET) } namespace Duration { def diff(fromNanos: Int, toNanos: Int) = toNanos - fromNanos } -def red(s: String) = s.withEscape(ANSI::RED) -def green(s: String) = s.withEscape(ANSI::GREEN) -def dim(s: String) = s.withEscape(ANSI::FAINT) -def bold(s: String) = s.withEscape(ANSI::BOLD) +def red(text: String) = text.withEscape(ANSI::RED) +def green(text: String) = text.withEscape(ANSI::GREEN) +def dim(text: String) = text.withEscape(ANSI::FAINT) +def bold(text: String) = text.withEscape(ANSI::BOLD) interface Assertion { def assert(condition: Bool, msg: String): Unit From 63252ed283e0134b9a97a48ccfc74b3abbb6d133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 30 Aug 2024 15:35:35 +0200 Subject: [PATCH 10/14] Add even more comments --- libraries/common/test.effekt | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/libraries/common/test.effekt b/libraries/common/test.effekt index c05edca1b..717fe6d94 100644 --- a/libraries/common/test.effekt +++ b/libraries/common/test.effekt @@ -114,8 +114,9 @@ def test(name: String) { body: => Unit / Assertion } = { } /// Run a test suite with a given `name`. -/// If `printTimes` is `true` (or missing), prints out time in milliseconds. -/// Formats automatically using ANSI escapes. +/// - If `printTimes` is `true` (or missing), prints out time in milliseconds. +/// - Formats automatically using ANSI escapes. +/// - Returns `true` if all tests succeed, otherwise returns `false`. If you want to exit the program on failure, see `mainSuite`. /// /// Example: /// ```effekt @@ -175,12 +176,18 @@ def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } return failed == 0 } +/// See `suite` above. +def suite(name: String) { body: => Unit / { Test, Formatted } }: Bool / {} = + suite(name, true) { body } + /// Use as `def main() = mainSuite("...") { ... }` -def mainSuite(name: String) { body: => Unit / Test }: Unit = { - val result = suite(name) { body() } +/// Recommended for standalone test files ran by CI. +/// +/// Exits after running all tests: +/// - if all tests succeed, exits the program with success (exit code 0) +/// - otherwise exits the program with failure (exit code 1) +def mainSuite(name: String) { body: => Unit / { Test, Formatted } }: Unit = { + val result = suite(name, true) { body } val exitCode = if (result) 0 else 1 exit(exitCode) } - -def suite(name: String) { body: => Unit / { Test, Formatted } }: Bool / {} = - suite(name, true) { body } From 51d7849a5e59e5cf9d9b55b94c2bc5c78d208f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 30 Aug 2024 15:55:27 +0200 Subject: [PATCH 11/14] Re-add forgotten explicit namespace --- libraries/common/test.effekt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/common/test.effekt b/libraries/common/test.effekt index 717fe6d94..e0a444393 100644 --- a/libraries/common/test.effekt +++ b/libraries/common/test.effekt @@ -22,7 +22,7 @@ namespace Formatted { def tryEmit(escape: String): String / Formatted = if (do supportsEscape(escape)) escape else "" - def withEscape(text: String, colorEscape: String): String / Formatted = + def colored(text: String, colorEscape: String): String / Formatted = tryEmit(colorEscape) ++ text ++ tryEmit(ANSI::RESET) } @@ -30,10 +30,10 @@ namespace Duration { def diff(fromNanos: Int, toNanos: Int) = toNanos - fromNanos } -def red(text: String) = text.withEscape(ANSI::RED) -def green(text: String) = text.withEscape(ANSI::GREEN) -def dim(text: String) = text.withEscape(ANSI::FAINT) -def bold(text: String) = text.withEscape(ANSI::BOLD) +def red(text: String) = Formatted::colored(text, ANSI::RED) +def green(text: String) = Formatted::colored(text, ANSI::GREEN) +def dim(text: String) = Formatted::colored(text, ANSI::FAINT) +def bold(text: String) = Formatted::colored(text, ANSI::BOLD) interface Assertion { def assert(condition: Bool, msg: String): Unit From 618cd746d137b17772fe0a8a863fa7f8a66c1ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Tue, 24 Sep 2024 21:07:46 +0200 Subject: [PATCH 12/14] Tests should now be supported on LLVM again --- effekt/jvm/src/test/scala/effekt/StdlibTests.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala index 9876bf1c1..f2d5cd5f2 100644 --- a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala +++ b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala @@ -80,8 +80,5 @@ class StdlibLLVMTests extends StdlibTests { examplesDir / "stdlib" / "list" / "build.effekt", examplesDir / "stdlib" / "string" / "strings.effekt", examplesDir / "stdlib" / "string" / "unicode.effekt", - - // missing support for top-level constants for ANSI escapes - examplesDir / "stdlib" / "test", ) } From 2da8f9dfeefe079185348f850018cbe4843f1706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Wed, 25 Sep 2024 14:49:03 +0200 Subject: [PATCH 13/14] Explicit import via node, modify comment to reflect NodeJS16.x and newer --- libraries/common/bench.effekt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/common/bench.effekt b/libraries/common/bench.effekt index 17ac3a6d4..f17aba5e5 100644 --- a/libraries/common/bench.effekt +++ b/libraries/common/bench.effekt @@ -27,9 +27,9 @@ extern llvm """ declare i32 @clock_gettime(i32, ptr) """ -// This will not be needed from NodeJS 16 and newer. +// This should not be needed when using NodeJS 16.x or newer. extern jsNode """ - const { performance } = require('perf_hooks'); + const { performance } = require('node:perf_hooks'); """ /** From 71676ab62f4c20b7e24155ee698d185d69728c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Tue, 1 Oct 2024 15:03:54 +0200 Subject: [PATCH 14/14] Separate out string/tty, make bench slightly more typesafe --- libraries/common/bench.effekt | 18 +++++--- libraries/common/string.effekt | 37 ----------------- libraries/common/string/tty.effekt | 66 ++++++++++++++++++++++++++++++ libraries/common/test.effekt | 35 +--------------- 4 files changed, 80 insertions(+), 76 deletions(-) create mode 100644 libraries/common/string/tty.effekt diff --git a/libraries/common/bench.effekt b/libraries/common/bench.effekt index f17aba5e5..7cb4e5f40 100644 --- a/libraries/common/bench.effekt +++ b/libraries/common/bench.effekt @@ -1,5 +1,7 @@ module bench +type Nanos = Int + /** * The current time (since UNIX Epoch) in nanoseconds. * @@ -8,7 +10,7 @@ module bench * - chez: Microseconds * - ml: Microseconds */ -extern io def timestamp(): Int = +extern io def timestamp(): Nanos = js "Date.now() * 1000000" chez "(timestamp)" ml "IntInf.toInt (Time.toNanoseconds (Time.now ()))" @@ -38,18 +40,24 @@ extern jsNode """ * This timestamp should only be used for **relative** measurements, * as gives no guarantees on the absolute time (unlike a UNIX timestamp). */ -extern io def relativeTimestamp(): Int = +extern io def relativeTimestamp(): Nanos = js "Math.round(performance.now() * 1000000)" default { timestamp() } +type Duration = Int + +namespace Duration { + def diff(fromNanos: Nanos, toNanos: Nanos): Duration = toNanos - fromNanos +} + /** * Runs the block and returns the time in nanoseconds */ -def timed { block: => Unit }: Int = { +def timed { block: => Unit }: Duration = { val before = relativeTimestamp() block() val after = relativeTimestamp() - after - before + Duration::diff(before, after) } def measure(warmup: Int, iterations: Int) { block: => Unit }: Unit = { @@ -67,7 +75,7 @@ def measure(warmup: Int, iterations: Int) { block: => Unit }: Unit = { /** * Takes a duration in nanoseconds and formats it in milliseconds with precision of two decimal digits. */ -def formatMs(nanos: Int): String = { +def formatMs(nanos: Nanos): String = { val micros = nanos / 1000000 val sub = (nanos.mod(1000000).toDouble / 10000.0).round diff --git a/libraries/common/string.effekt b/libraries/common/string.effekt index a4f42c638..ed6896c77 100644 --- a/libraries/common/string.effekt +++ b/libraries/common/string.effekt @@ -243,43 +243,6 @@ def string[T] { prog: => T / Stream }: (T, String) = <> def printing[T] { prog: => T / Stream }: T = <> -// ANSI escape codes -namespace ANSI { - val CSI = "\u001b[" - - def escape(s: String) = CSI ++ s ++ "m" - - val BLACK = escape("30") - val RED = escape("31") - val GREEN = escape("32") - val YELLOW = escape("33") - val BLUE = escape("34") - val MAGENTA = escape("35") - val CYAN = escape("36") - val WHITE = escape("37") - - val BG_BLACK = escape("40") - val BG_RED = escape("41") - val BG_GREEN = escape("42") - val BG_YELLOW = escape("43") - val BG_BLUE = escape("44") - val BG_MAGENTA = escape("45") - val BG_CYAN = escape("46") - val BG_WHITE = escape("47") - - val RESET = escape("0") - - val BOLD = escape("1") - val FAINT = escape("2") - val ITALIC = escape("3") - val UNDERLINE = escape("4") - val BLINK = escape("5") - val REVERSE = escape("7") - val CROSSOUT = escape("9") - val OVERLINE = escape("53") -} - - // Characters // ---------- // diff --git a/libraries/common/string/tty.effekt b/libraries/common/string/tty.effekt new file mode 100644 index 000000000..048f9bd55 --- /dev/null +++ b/libraries/common/string/tty.effekt @@ -0,0 +1,66 @@ +module string/tty + +// ANSI escape codes +namespace ANSI { + val CSI = "\u001b[" + + def escape(s: String) = CSI ++ s ++ "m" + + val BLACK = escape("30") + val RED = escape("31") + val GREEN = escape("32") + val YELLOW = escape("33") + val BLUE = escape("34") + val MAGENTA = escape("35") + val CYAN = escape("36") + val WHITE = escape("37") + + val BG_BLACK = escape("40") + val BG_RED = escape("41") + val BG_GREEN = escape("42") + val BG_YELLOW = escape("43") + val BG_BLUE = escape("44") + val BG_MAGENTA = escape("45") + val BG_CYAN = escape("46") + val BG_WHITE = escape("47") + + val RESET = escape("0") + + val BOLD = escape("1") + val FAINT = escape("2") + val ITALIC = escape("3") + val UNDERLINE = escape("4") + val BLINK = escape("5") + val REVERSE = escape("7") + val CROSSOUT = escape("9") + val OVERLINE = escape("53") +} + +def red(text: String) = Formatted::colored(text, ANSI::RED) +def green(text: String) = Formatted::colored(text, ANSI::GREEN) +def dim(text: String) = Formatted::colored(text, ANSI::FAINT) +def bold(text: String) = Formatted::colored(text, ANSI::BOLD) + +interface Formatted { + def supportsEscape(escape: String): Bool +} + +namespace Formatted { + /// Run given block of code, allowing all formatting + def formatting[R] { prog : => R / Formatted }: R = + try { prog() } with Formatted { + def supportsEscape(escape: String) = resume(true) + } + + /// Run given block of code, ignoring all formatting + def noFormatting[R] { prog : => R / Formatted }: R = + try { prog() } with Formatted { + def supportsEscape(escape: String) = resume(false) + } + + def tryEmit(escape: String): String / Formatted = + if (do supportsEscape(escape)) escape else "" + + def colored(text: String, colorEscape: String): String / Formatted = + tryEmit(colorEscape) ++ text ++ tryEmit(ANSI::RESET) +} \ No newline at end of file diff --git a/libraries/common/test.effekt b/libraries/common/test.effekt index e0a444393..cca0731d7 100644 --- a/libraries/common/test.effekt +++ b/libraries/common/test.effekt @@ -1,40 +1,7 @@ -import string +import string/tty import process import bench -interface Formatted { - def supportsEscape(escape: String): Bool -} - -namespace Formatted { - /// Run given block of code, allowing all formatting - def formatting[R] { prog : => R / Formatted }: R = - try { prog() } with Formatted { - def supportsEscape(escape: String) = resume(true) - } - - /// Run given block of code, ignoring all formatting - def noFormatting[R] { prog : => R / Formatted }: R = - try { prog() } with Formatted { - def supportsEscape(escape: String) = resume(false) - } - - def tryEmit(escape: String): String / Formatted = - if (do supportsEscape(escape)) escape else "" - - def colored(text: String, colorEscape: String): String / Formatted = - tryEmit(colorEscape) ++ text ++ tryEmit(ANSI::RESET) -} - -namespace Duration { - def diff(fromNanos: Int, toNanos: Int) = toNanos - fromNanos -} - -def red(text: String) = Formatted::colored(text, ANSI::RED) -def green(text: String) = Formatted::colored(text, ANSI::GREEN) -def dim(text: String) = Formatted::colored(text, ANSI::FAINT) -def bold(text: String) = Formatted::colored(text, ANSI::BOLD) - interface Assertion { def assert(condition: Bool, msg: String): Unit }