From cbaf4492582f19f30eea805f01b4b0894222d3c5 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Fri, 23 May 2025 15:40:30 +0100 Subject: [PATCH 01/11] Decode panics --- gleam.toml | 4 +- manifest.toml | 4 +- src/gleeunit/internal/gleam_panic.gleam | 52 ++++++ src/gleeunit/internal/gleam_panic_ffi.erl | 47 ++++++ src/gleeunit/internal/gleam_panic_ffi.mjs | 88 ++++++++++ test/gleam_panics_test.gleam | 193 ++++++++++++++++++++++ test/gleeunit_test_ffi.erl | 9 + test/gleeunit_test_ffi.mjs | 9 + 8 files changed, 402 insertions(+), 4 deletions(-) create mode 100644 src/gleeunit/internal/gleam_panic.gleam create mode 100644 src/gleeunit/internal/gleam_panic_ffi.erl create mode 100644 src/gleeunit/internal/gleam_panic_ffi.mjs create mode 100644 test/gleam_panics_test.gleam create mode 100644 test/gleeunit_test_ffi.erl create mode 100644 test/gleeunit_test_ffi.mjs diff --git a/gleam.toml b/gleam.toml index bcd1a7d..d825ae9 100644 --- a/gleam.toml +++ b/gleam.toml @@ -4,10 +4,10 @@ licences = ["Apache-2.0"] description = "Gleam bindings to Erlang's EUnit test framework" repository = { type = "github", user = "lpil", repo = "gleeunit" } links = [{ title = "Sponsor", href = "https://github.com/sponsors/lpil" }] -gleam = ">= 0.33.0" +gleam = ">= 1.11.0" [javascript.deno] allow_read = ["gleam.toml", "test", "build"] [dependencies] -gleam_stdlib = ">= 0.33.0 and < 2.0.0" +gleam_stdlib = ">= 0.60.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index 3d22c1b..a838186 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,8 +2,8 @@ # You typically do not need to edit this file packages = [ - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, + { name = "gleam_stdlib", version = "0.60.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "621D600BB134BC239CB2537630899817B1A42E60A1D46C5E9F3FAE39F88C800B" }, ] [requirements] -gleam_stdlib = { version = ">= 0.33.0 and < 2.0.0" } +gleam_stdlib = { version = ">= 0.60.0 and < 2.0.0" } diff --git a/src/gleeunit/internal/gleam_panic.gleam b/src/gleeunit/internal/gleam_panic.gleam new file mode 100644 index 0000000..4ff62a7 --- /dev/null +++ b/src/gleeunit/internal/gleam_panic.gleam @@ -0,0 +1,52 @@ +import gleam/dynamic + +pub type GleamPanic { + GleamPanic( + module: String, + function: String, + line: Int, + message: String, + kind: PanicKind, + ) +} + +pub type PanicKind { + Todo + Panic + LetAssert( + start: Int, + pattern_start: Int, + pattern_end: Int, + value: dynamic.Dynamic, + ) + Assert( + start: Int, + expression_start: Int, + expression_end: Int, + kind: AssertKind, + ) +} + +pub type AssertKind { + BinaryOperator( + operator: String, + left: AssertedExpression, + right: AssertedExpression, + ) + FunctionCall(arguments: List(AssertedExpression)) + OtherExpression(expression: AssertedExpression) +} + +pub type AssertedExpression { + AssertedExpression(start: Int, end: Int, kind: ExpressionKind) +} + +pub type ExpressionKind { + Literal(value: dynamic.Dynamic) + Expression(value: dynamic.Dynamic) + Unevaluated +} + +@external(erlang, "gleam_panic_ffi", "from_dynamic") +@external(javascript, "./gleam_panic_ffi.mjs", "from_dynamic") +pub fn from_dynamic(data: dynamic.Dynamic) -> Result(GleamPanic, Nil) diff --git a/src/gleeunit/internal/gleam_panic_ffi.erl b/src/gleeunit/internal/gleam_panic_ffi.erl new file mode 100644 index 0000000..4adbffa --- /dev/null +++ b/src/gleeunit/internal/gleam_panic_ffi.erl @@ -0,0 +1,47 @@ +-module(gleam_panic_ffi). +-export([from_dynamic/1]). + +from_dynamic(#{ + gleam_error := assert, + start := Start, + expression_start := EStart, + expression_end := EEnd +} = E) -> + wrap(E, {assert, Start, EStart, EEnd, assert_kind(E)}); +from_dynamic(#{ + gleam_error := let_assert, + start := Start, + pattern_start := PStart, + pattern_end := PEnd, + value := Value +} = E) -> + wrap(E, {let_assert, Start, PStart, PEnd, Value}); +from_dynamic(#{gleam_error := panic} = E) -> + wrap(E, panic); +from_dynamic(#{gleam_error := todo} = E) -> + wrap(E, todo); +from_dynamic(_) -> + {error, nil}. + +assert_kind(#{kind := binary_operator, left := L, right := R, operator := O}) -> + {binary_operator, atom_to_binary(O), expression(L), expression(R)}; +assert_kind(#{kind := function_call, arguments := Arguments}) -> + {function_call, lists:map(fun expression/1, Arguments)}; +assert_kind(#{kind := expression, expression := Expression}) -> + {other_expression, expression(Expression)}. + +expression(#{start := S, 'end' := E, kind := literal, value := Value}) -> + {asserted_expression, S, E, {literal, Value}}; +expression(#{start := S, 'end' := E, kind := expression, value := Value}) -> + {asserted_expression, S, E, {expression, Value}}; +expression(#{start := S, 'end' := E, kind := unevaluated}) -> + {asserted_expression, S, E, unevaluated}. + +wrap(#{ + gleam_error := _, + module := Module, + function := Function, + line := Line, + message := Message +}, Kind) -> + {ok, {gleam_panic, Module, Function, Line, Message, Kind}}. diff --git a/src/gleeunit/internal/gleam_panic_ffi.mjs b/src/gleeunit/internal/gleam_panic_ffi.mjs new file mode 100644 index 0000000..2211dac --- /dev/null +++ b/src/gleeunit/internal/gleam_panic_ffi.mjs @@ -0,0 +1,88 @@ +import { Ok, Error, Empty, NonEmpty } from "../../gleam.mjs"; +import { + GleamPanic, + Todo, + Panic, + LetAssert, + Assert, + BinaryOperator, + FunctionCall, + OtherExpression, + AssertedExpression, + Literal, + Expression, + Unevaluated, +} from "./gleam_panic.mjs"; + +export function from_dynamic(error) { + if (!(error instanceof globalThis.Error) || !error.gleam_error) { + return new Error(undefined); + } + + if (error.gleam_error === "todo") { + return wrap(error, new Todo()); + } + + if (error.gleam_error === "panic") { + return wrap(error, new Panic()); + } + + if (error.gleam_error === "let_assert") { + let kind = new LetAssert( + error.start, + error.pattern_start, + error.pattern_end, + error.value, + ); + return wrap(error, kind); + } + + if (error.gleam_error === "assert") { + let kind = new Assert( + error.start, + error.expression_start, + error.expression_end, + assert_kind(error), + ); + return wrap(error, kind); + } + + return new Error(undefined); +} + +function assert_kind(error) { + if (error.kind == "binary_operator") { + return new BinaryOperator( + error.operator, + expression(error.left), + expression(error.right), + ); + } + + if (error.kind == "function_call") { + let list = new Empty(); + let i = error.arguments.length; + while (i--) { + list = new NonEmpty(expression(error.arguments[i]), list); + } + return new FunctionCall(list); + } + + return new OtherExpression(expression(error.expression)); +} + +function expression(data) { + const expression = new AssertedExpression(data.start, data.end, undefined); + if (data.kind == "literal") { + expression.kind = new Literal(data.value); + } else if (data.kind == "expression") { + expression.kind = new Expression(data.value); + } else { + expression.kind = new Unevaluated(); + } + return expression; +} + +function wrap(e, kind) { + return new Ok(new GleamPanic(e.module, e.function, e.line, e.message, kind)); +} diff --git a/test/gleam_panics_test.gleam b/test/gleam_panics_test.gleam new file mode 100644 index 0000000..c209240 --- /dev/null +++ b/test/gleam_panics_test.gleam @@ -0,0 +1,193 @@ +import gleam/dynamic +import gleam/function +import gleeunit/internal/gleam_panic.{ + Assert, BinaryOperator, Expression, FunctionCall, LetAssert, Literal, + OtherExpression, Panic, Todo, Unevaluated, +} + +@external(erlang, "gleeunit_test_ffi", "rescue") +@external(javascript, "./gleeunit_test_ffi.mjs", "rescue") +fn rescue(f: fn() -> t) -> Result(t, dynamic.Dynamic) + +pub fn panic_test() { + let assert Error(e) = rescue(fn() { panic }) + let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.kind == Panic + assert e.function == "panic_test" + assert e.module == "gleam_panics_test" + assert e.line > 1 + assert e.message == "`panic` expression evaluated." +} + +pub fn panic_message_test() { + let assert Error(e) = rescue(fn() { panic as "oh my!" }) + let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.kind == Panic + assert e.function == "panic_message_test" + assert e.module == "gleam_panics_test" + assert e.line > 1 + assert e.message == "oh my!" +} + +pub fn todo_test() { + let assert Error(e) = rescue(fn() { todo }) + let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.kind == Todo + assert e.function == "todo_test" + assert e.module == "gleam_panics_test" + assert e.line > 1 + assert e.message + == "`todo` expression evaluated. This code has not yet been implemented." +} + +pub fn todo_message_test() { + let assert Error(e) = rescue(fn() { todo as "oh my!" }) + let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.kind == Todo + assert e.function == "todo_message_test" + assert e.module == "gleam_panics_test" + assert e.line > 1 + assert e.message == "oh my!" +} + +pub fn let_assert_test() { + let assert Error(e) = + rescue(fn() { + let assert 0 = function.identity(123) + }) + let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.function == "let_assert_test" + assert e.module == "gleam_panics_test" + assert e.line > 1 + assert e.message == "Pattern match failed, no pattern matched the value." + let assert LetAssert(value:, start:, pattern_start:, pattern_end:) = e.kind + assert value == dynamic.int(123) + assert start > 1 + assert pattern_start == start + 11 + assert pattern_end == pattern_start + 1 +} + +pub fn let_assert_message_test() { + let assert Error(e) = + rescue(fn() { + let assert 0 = function.identity(321) as "oh dear" + }) + let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.function == "let_assert_message_test" + assert e.module == "gleam_panics_test" + assert e.line > 1 + assert e.message == "oh dear" + let assert LetAssert(value:, start:, pattern_start:, pattern_end:) = e.kind + assert value == dynamic.int(321) + assert start > 1 + assert pattern_start == start + 11 + assert pattern_end == pattern_start + 1 +} + +pub fn assert_expression_test() { + let assert Error(e) = + rescue(fn() { + let x = function.identity(False) + assert x + }) + let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.function == "assert_expression_test" + assert e.module == "gleam_panics_test" + assert e.line > 1 + assert e.message == "Assertion failed." + let assert Assert(start:, expression_start:, expression_end:, kind:) = e.kind + assert start > 1 + assert expression_start == start + 7 + assert expression_end == expression_start + 1 + let assert OtherExpression(expression:) = kind + assert expression.start == expression_start + assert expression.end == expression_end + assert expression.kind == Expression(value: dynamic.bool(False)) +} + +pub fn assert_expression_message_test() { + let assert Error(e) = + rescue(fn() { + let x = function.identity(False) + assert x as "maybe?" + }) + let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.function == "assert_expression_message_test" + assert e.module == "gleam_panics_test" + assert e.line > 1 + assert e.message == "maybe?" + let assert Assert(start:, expression_start:, expression_end:, kind:) = e.kind + assert start > 1 + assert expression_start == start + 7 + assert expression_end == expression_start + 1 + let assert OtherExpression(expression:) = kind + assert expression.start == expression_start + assert expression.end == expression_end + assert expression.kind == Expression(value: dynamic.bool(False)) +} + +pub fn assert_function_test() { + let assert Error(e) = + rescue(fn() { + assert function.identity(False) + }) + let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.function == "assert_function_test" + assert e.module == "gleam_panics_test" + assert e.line > 1 + assert e.message == "Assertion failed." + let assert Assert(start:, expression_start:, expression_end:, kind:) = e.kind + assert start > 1 + assert expression_start == start + 7 + assert expression_end == expression_start + 24 + let assert FunctionCall(arguments: [expression]) = kind + assert expression.start == expression_start + 18 + assert expression.end == expression_end - 1 + assert expression.kind == Literal(value: dynamic.bool(False)) +} + +pub fn assert_function_message_test() { + let assert Error(e) = + rescue(fn() { + assert function.identity(False) as "oh!" + }) + let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.function == "assert_function_message_test" + assert e.module == "gleam_panics_test" + assert e.line > 1 + assert e.message == "oh!" + let assert Assert(start:, expression_start:, expression_end:, kind:) = e.kind + assert start > 1 + assert expression_start == start + 7 + assert expression_end == expression_start + 24 + let assert FunctionCall(arguments: [expression]) = kind + assert expression.start == expression_start + 18 + assert expression.end == expression_end - 1 + assert expression.kind == Literal(value: dynamic.bool(False)) +} + +pub fn assert_binary_operator_test() { + let assert Error(e) = + rescue(fn() { + let a = False + assert a && function.identity(False) + }) + let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.function == "assert_binary_operator_test" + assert e.module == "gleam_panics_test" + assert e.line > 1 + assert e.message == "Assertion failed." + let assert Assert(start:, expression_start:, expression_end:, kind:) = e.kind + assert start > 1 + assert expression_start == start + 7 + assert expression_end == expression_start + 29 + let assert BinaryOperator(operator:, left:, right:) = kind + assert operator == "&&" + assert left.start == expression_start + assert left.end == left.start + 1 + assert left.kind == Expression(dynamic.bool(False)) + assert right.start == left.end + 4 + assert right.end == right.start + 24 + assert right.end == expression_end + assert right.kind == Unevaluated +} diff --git a/test/gleeunit_test_ffi.erl b/test/gleeunit_test_ffi.erl new file mode 100644 index 0000000..bb26f40 --- /dev/null +++ b/test/gleeunit_test_ffi.erl @@ -0,0 +1,9 @@ +-module(gleeunit_test_ffi). +-export([rescue/1]). + +rescue(F) -> + try + {ok, F()} + catch + _:Error:_ -> {error, Error} + end. diff --git a/test/gleeunit_test_ffi.mjs b/test/gleeunit_test_ffi.mjs new file mode 100644 index 0000000..783209d --- /dev/null +++ b/test/gleeunit_test_ffi.mjs @@ -0,0 +1,9 @@ +import { Ok, Error } from "./gleam.mjs"; + +export function rescue(f) { + try { + return new Ok(f()); + } catch (e) { + return new Error(e); + } +} From 702ad0e3e339c087cbbe2121f604ce1064a86d32 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Sun, 25 May 2025 00:31:15 +0100 Subject: [PATCH 02/11] Test result reporting --- src/gleeunit/internal/gleam_panic.gleam | 3 +- src/gleeunit/internal/gleam_panic_ffi.erl | 7 +- src/gleeunit/internal/gleam_panic_ffi.mjs | 4 +- src/gleeunit/internal/reporting.gleam | 207 ++++++++ src/gleeunit_ffi.erl | 1 - src/gleeunit_ffi.mjs | 11 + src/gleeunit_progress.erl | 614 ++-------------------- test/gleam_panics_test.gleam | 15 + 8 files changed, 282 insertions(+), 580 deletions(-) create mode 100644 src/gleeunit/internal/reporting.gleam diff --git a/src/gleeunit/internal/gleam_panic.gleam b/src/gleeunit/internal/gleam_panic.gleam index 4ff62a7..7765da5 100644 --- a/src/gleeunit/internal/gleam_panic.gleam +++ b/src/gleeunit/internal/gleam_panic.gleam @@ -2,10 +2,11 @@ import gleam/dynamic pub type GleamPanic { GleamPanic( + message: String, + file: String, module: String, function: String, line: Int, - message: String, kind: PanicKind, ) } diff --git a/src/gleeunit/internal/gleam_panic_ffi.erl b/src/gleeunit/internal/gleam_panic_ffi.erl index 4adbffa..b159100 100644 --- a/src/gleeunit/internal/gleam_panic_ffi.erl +++ b/src/gleeunit/internal/gleam_panic_ffi.erl @@ -39,9 +39,10 @@ expression(#{start := S, 'end' := E, kind := unevaluated}) -> wrap(#{ gleam_error := _, + file := File, + message := Message, module := Module, function := Function, - line := Line, - message := Message + line := Line }, Kind) -> - {ok, {gleam_panic, Module, Function, Line, Message, Kind}}. + {ok, {gleam_panic, Message, File, Module, Function, Line, Kind}}. diff --git a/src/gleeunit/internal/gleam_panic_ffi.mjs b/src/gleeunit/internal/gleam_panic_ffi.mjs index 2211dac..ef2b818 100644 --- a/src/gleeunit/internal/gleam_panic_ffi.mjs +++ b/src/gleeunit/internal/gleam_panic_ffi.mjs @@ -84,5 +84,7 @@ function expression(data) { } function wrap(e, kind) { - return new Ok(new GleamPanic(e.module, e.function, e.line, e.message, kind)); + return new Ok( + new GleamPanic(e.message, e.file, e.module, e.function, e.line, kind), + ); } diff --git a/src/gleeunit/internal/reporting.gleam b/src/gleeunit/internal/reporting.gleam new file mode 100644 index 0000000..8d6c3ea --- /dev/null +++ b/src/gleeunit/internal/reporting.gleam @@ -0,0 +1,207 @@ +import gleam/bit_array +import gleam/dynamic +import gleam/int +import gleam/io +import gleam/option.{type Option} +import gleam/result +import gleam/string +import gleeunit/internal/gleam_panic.{type GleamPanic} + +pub type State { + State(passed: Int, failed: Int, skipped: Int) +} + +pub fn new_state() -> State { + State(passed: 0, failed: 0, skipped: 0) +} + +pub fn finished(state: State) -> Int { + case state { + State(passed: 0, failed: 0, skipped: 0) -> { + io.println("\nNo tests found!") + 1 + } + State(failed: 0, skipped: 0, ..) -> { + let message = "\n" <> int.to_string(state.passed) <> " tests, no failures" + io.println(green(message)) + 0 + } + State(skipped: 0, ..) -> { + let message = + "\n" + <> int.to_string(state.passed) + <> " tests, " + <> int.to_string(state.failed) + <> " failures" + io.println(red(message)) + 0 + } + State(failed: 0, ..) -> { + let message = + "\n" + <> int.to_string(state.passed) + <> " tests, 0 failures, " + <> int.to_string(state.skipped) + <> " skipped" + io.println(yellow(message)) + 1 + } + State(..) -> { + let message = + "\n" + <> int.to_string(state.passed) + <> " tests, " + <> int.to_string(state.failed) + <> " failures, " + <> " skipped" + io.println(red(message)) + 1 + } + } +} + +pub fn test_passed(state: State) -> State { + io.print(green(".")) + State(..state, passed: state.passed + 1) +} + +pub fn test_failed( + state: State, + module: String, + function: String, + error: dynamic.Dynamic, +) -> State { + let message = case gleam_panic.from_dynamic(error) { + Ok(error) -> { + let src = option.from_result(read_file(error.file)) + format_gleam_error(error, module, function, src) + } + Error(_) -> format_unknown(error) + } + + io.print("\n" <> message) + State(..state, failed: state.failed + 1) +} + +fn format_unknown(error: dynamic.Dynamic) -> String { + "\nAn unexpected error occurred:\n\n" <> string.inspect(error) +} + +fn format_gleam_error( + error: GleamPanic, + module: String, + function: String, + src: Option(BitArray), +) -> String { + let location = grey(error.file <> ":" <> int.to_string(error.line)) + + case error.kind { + gleam_panic.Panic -> { + string.concat([ + bold(red("panic")) <> " " <> location <> "\n", + blue(" test") <> ": " <> module <> "." <> function <> "\n", + blue(" info") <> ": " <> error.message <> "\n", + ]) + } + + gleam_panic.Todo -> { + string.concat([ + bold(yellow("todo")) <> " " <> location <> "\n", + blue(" test") <> ": " <> module <> "." <> function <> "\n", + blue(" info") <> ": " <> error.message <> "\n", + ]) + } + + gleam_panic.Assert(start:, expression_end:, kind:, ..) -> { + string.concat([ + bold(red("assert")) <> " " <> location <> "\n", + blue(" test") <> ": " <> module <> "." <> function <> "\n", + code_snippet(src, start, expression_end), + assert_info(kind), + blue(" info") <> ": " <> error.message <> "\n", + ]) + } + + // TODO: include the whole expression + gleam_panic.LetAssert(start:, pattern_end:, value:, ..) -> { + string.concat([ + bold(red("let assert")) <> " " <> location <> "\n", + blue(" test") <> ": " <> module <> "." <> function <> "\n", + code_snippet(src, start, pattern_end), + blue("value") <> ": " <> string.inspect(value) <> "\n", + blue(" info") <> ": " <> error.message <> "\n", + ]) + } + } +} + +fn assert_info(kind: gleam_panic.AssertKind) -> String { + case kind { + gleam_panic.BinaryOperator(operator:, left:, right:) -> + string.concat([assert_value(" left", left), assert_value("right", right)]) + + gleam_panic.FunctionCall(arguments:) -> todo + + gleam_panic.OtherExpression(expression:) -> "" + } +} + +fn assert_value(name: String, value: gleam_panic.AssertedExpression) -> String { + case value.kind { + gleam_panic.Expression(value:) -> + blue(name) <> ": " <> string.inspect(value) <> "\n" + + gleam_panic.Literal(..) | gleam_panic.Unevaluated -> "" + } +} + +fn code_snippet(src: Option(BitArray), start: Int, end: Int) -> String { + { + use src <- result.try(option.to_result(src, Nil)) + use snippet <- result.try(bit_array.slice(src, start, end - start)) + use snippet <- result.try(bit_array.to_string(snippet)) + let snippet = blue(" code") <> ": " <> snippet <> "\n" + Ok(snippet) + } + |> result.unwrap("") +} + +pub fn test_skipped(state: State, module: String, function: String) -> State { + io.print("\n" <> module <> "." <> function <> yellow(" skipped")) + State(..state, skipped: state.skipped + 1) +} + +fn bold(text: String) -> String { + "\u{001b}[1m" <> text <> "\u{001b}[22m" +} + +fn blue(text: String) -> String { + "\u{001b}[34m" <> text <> "\u{001b}[39m" +} + +fn yellow(text: String) -> String { + "\u{001b}[33m" <> text <> "\u{001b}[39m" +} + +fn green(text: String) -> String { + "\u{001b}[32m" <> text <> "\u{001b}[39m" +} + +fn red(text: String) -> String { + "\u{001b}[31m" <> text <> "\u{001b}[39m" +} + +fn grey(text: String) -> String { + "\u{001b}[90m" <> text <> "\u{001b}[39m" +} + +@external(erlang, "file", "read_file") +fn read_file(path: String) -> Result(BitArray, dynamic.Dynamic) { + case read_file_text(path) { + Ok(text) -> Ok(bit_array.from_string(text)) + Error(e) -> Error(e) + } +} + +@external(javascript, "../../gleeunit_ffi.mjs", "read_file") +fn read_file_text(path: String) -> Result(String, dynamic.Dynamic) diff --git a/src/gleeunit_ffi.erl b/src/gleeunit_ffi.erl index 18d593b..e57cc93 100644 --- a/src/gleeunit_ffi.erl +++ b/src/gleeunit_ffi.erl @@ -9,7 +9,6 @@ find_files(Pattern, In) -> Results = filelib:wildcard(binary_to_list(Pattern), binary_to_list(In)), lists:map(fun list_to_binary/1, Results). - should_equal(Actual, Expected) -> ?assertEqual(Expected, Actual), nil. diff --git a/src/gleeunit_ffi.mjs b/src/gleeunit_ffi.mjs index 339a843..2ead732 100644 --- a/src/gleeunit_ffi.mjs +++ b/src/gleeunit_ffi.mjs @@ -1,3 +1,14 @@ +import { readFileSync } from "fs"; +import { Ok, Error as GleamError } from "./gleam.mjs"; + +export function read_file(path) { + try { + return new Ok(readFileSync(path)); + } catch { + return new GleamError(undefined); + } +} + async function* gleamFiles(directory) { for (let entry of await read_dir(directory)) { let path = join_path(directory, entry); diff --git a/src/gleeunit_progress.erl b/src/gleeunit_progress.erl index acd9abf..65250d8 100644 --- a/src/gleeunit_progress.erl +++ b/src/gleeunit_progress.erl @@ -1,607 +1,73 @@ %% A formatter adapted from Sean Cribb's https://github.com/seancribbs/eunit_formatters -%% @doc A listener/reporter for eunit that prints '.' for each -%% success, 'F' for each failure, and 'E' for each error. It can also -%% optionally summarize the failures at the end. --compile({nowarn_unused_function, [insert/2, to_list/1, to_list/2, size/1]}). -module(gleeunit_progress). -behaviour(eunit_listener). -define(NOTEST, true). -include_lib("eunit/include/eunit.hrl"). --define(RED, "\e[0;31m"). --define(GREEN, "\e[0;32m"). --define(YELLOW, "\e[0;33m"). --define(WHITE, "\e[0;37m"). --define(CYAN, "\e[0;36m"). --define(RESET, "\e[0m"). - --record(node,{ - rank = 0 :: non_neg_integer(), - key :: term(), - value :: term(), - children = new() :: binomial_heap() - }). - --export_type([binomial_heap/0, heap_node/0]). --type binomial_heap() :: [ heap_node() ]. --type heap_node() :: #node{}. - %% eunit_listener callbacks -export([ - init/1, - handle_begin/3, - handle_end/3, - handle_cancel/3, - terminate/2, - start/0, - start/1 - ]). - -%% -- binomial_heap.erl content start -- - --record(state, { - status = dict:new() :: euf_dict(), - failures = [] :: [[pos_integer()]], - skips = [] :: [[pos_integer()]], - timings = new() :: binomial_heap(), - colored = true :: boolean(), - profile = false :: boolean() - }). - --type euf_dict() :: dict:dict(). - --spec new() -> binomial_heap(). -new() -> - []. - -% Inserts a new pair into the heap (or creates a new heap) --spec insert(term(), term()) -> binomial_heap(). -insert(Key,Value) -> - insert(Key,Value,[]). - --spec insert(term(), term(), binomial_heap()) -> binomial_heap(). -insert(Key,Value,Forest) -> - insTree(#node{key=Key,value=Value},Forest). - -% Merges two heaps --spec merge(binomial_heap(), binomial_heap()) -> binomial_heap(). -merge(TS1,[]) when is_list(TS1) -> TS1; -merge([],TS2) when is_list(TS2) -> TS2; -merge([#node{rank=R1}=T1|TS1]=F1,[#node{rank=R2}=T2|TS2]=F2) -> - if - R1 < R2 -> - [T1 | merge(TS1,F2)]; - R2 < R1 -> - [T2 | merge(F1, TS2)]; - true -> - insTree(link(T1,T2),merge(TS1,TS2)) - end. - -% Deletes the top entry from the heap and returns it --spec delete(binomial_heap()) -> {{term(), term()}, binomial_heap()}. -delete(TS) -> - {#node{key=Key,value=Value,children=TS1},TS2} = getMin(TS), - {{Key,Value},merge(lists:reverse(TS1),TS2)}. - -% Turns the heap into list in heap order --spec to_list(binomial_heap()) -> [{term(), term()}]. -to_list([]) -> []; -to_list(List) when is_list(List) -> - to_list([],List). -to_list(Acc, []) -> - lists:reverse(Acc); -to_list(Acc,Forest) -> - {Next, Trees} = delete(Forest), - to_list([Next|Acc], Trees). - -% Take N elements from the top of the heap --spec take(non_neg_integer(), binomial_heap()) -> [{term(), term()}]. -take(N,Trees) when is_integer(N), is_list(Trees) -> - take(N,Trees,[]). -take(0,_Trees,Acc) -> - lists:reverse(Acc); -take(_N,[],Acc)-> - lists:reverse(Acc); -take(N,Trees,Acc) -> - {Top,T2} = delete(Trees), - take(N-1,T2,[Top|Acc]). - -% Get an estimate of the size based on the binomial property --spec size(binomial_heap()) -> non_neg_integer(). -size(Forest) -> - erlang:trunc(lists:sum([math:pow(2,R) || #node{rank=R} <- Forest])). + init/1, handle_begin/3, handle_end/3, handle_cancel/3, terminate/2, + start/0, start/1 +]). -%% Private API --spec link(heap_node(), heap_node()) -> heap_node(). -link(#node{rank=R,key=X1,children=C1}=T1,#node{key=X2,children=C2}=T2) -> - case X1 < X2 of - true -> - T1#node{rank=R+1,children=[T2|C1]}; - _ -> - T2#node{rank=R+1,children=[T1|C2]} - end. - -insTree(Tree, []) -> - [Tree]; -insTree(#node{rank=R1}=T1, [#node{rank=R2}=T2|Rest] = TS) -> - case R1 < R2 of - true -> - [T1|TS]; - _ -> - insTree(link(T1,T2),Rest) - end. - -getMin([T]) -> - {T,[]}; -getMin([#node{key=K} = T|TS]) -> - {#node{key=K1} = T1,TS1} = getMin(TS), - case K < K1 of - true -> {T,TS}; - _ -> {T1,[T|TS1]} - end. +-define(reporting, gleeunit@internal@reporting). -%% -- binomial_heap.erl content end -- - -%% Startup start() -> start([]). start(Options) -> eunit_listener:start(?MODULE, Options). -%%------------------------------------------ -%% eunit_listener callbacks -%%------------------------------------------ -init(Options) -> - #state{colored=proplists:get_bool(colored, Options), - profile=proplists:get_bool(profile, Options)}. - -handle_begin(group, Data, St) -> - GID = proplists:get_value(id, Data), - Dict = St#state.status, - St#state{status=dict:store(GID, orddict:from_list([{type, group}|Data]), Dict)}; -handle_begin(test, Data, St) -> - TID = proplists:get_value(id, Data), - Dict = St#state.status, - St#state{status=dict:store(TID, orddict:from_list([{type, test}|Data]), Dict)}. +init(_Options) -> + ?reporting:new_state(). -handle_end(group, Data, St) -> - St#state{status=merge_on_end(Data, St#state.status)}; -handle_end(test, Data, St) -> - NewStatus = merge_on_end(Data, St#state.status), - St1 = print_progress(Data, St), - St2 = record_timing(Data, St1), - St2#state{status=NewStatus}. +handle_begin(_test_or_group, _data, State) -> + State. -handle_cancel(_, Data, #state{status=Status, skips=Skips}=St) -> - Status1 = merge_on_end(Data, Status), - ID = proplists:get_value(id, Data), - St#state{status=Status1, skips=[ID|Skips]}. +handle_end(group, _data, State) -> + State; +handle_end(test, Data, State) -> + {AtomModule, AtomFunction, _Arity} = proplists:get_value(source, Data), + Module = erlang:atom_to_binary(AtomModule), + Function = erlang:atom_to_binary(AtomFunction), -terminate({ok, Data}, St) -> - print_failures(St), - print_pending(St), - print_profile(St), - print_timing(St), - print_results(Data, St); -terminate({error, Reason}, St) -> - io:nl(), io:nl(), - print_colored(io_lib:format("Eunit failed: ~25p~n", [Reason]), ?RED, St), - sync_end(error). + % EUnit swallows stdout, so print it to make debugging easier. + case proplists:get_value(output, Data) of + undefined -> ok; + <<>> -> ok; + Out -> gleam@io:print(Out) + end, -sync_end(Result) -> - receive - {stop, Reference, ReplyTo} -> - ReplyTo ! {result, Reference, Result}, - ok - end. - -%%------------------------------------------ -%% Print and collect information during run -%%------------------------------------------ -print_progress(Data, St) -> - TID = proplists:get_value(id, Data), case proplists:get_value(status, Data) of ok -> - print_progress_success(St), - St; + ?reporting:test_passed(State); {skipped, _Reason} -> - print_progress_skipped(St), - St#state{skips=[TID|St#state.skips]}; - {error, Exception} -> - print_progress_failed(Exception, St), - St#state{failures=[TID|St#state.failures]} + ?reporting:test_skipped(State, Module, Function); + {error, {_, Exception, _Stack}} -> + ?reporting:test_failed(State, Module, Function, Exception) end. -record_timing(Data, State=#state{timings=T, profile=true}) -> - TID = proplists:get_value(id, Data), - case lists:keyfind(time, 1, Data) of - {time, Int} -> - %% It's a min-heap, so we insert negative numbers instead - %% of the actuals and normalize when we report on them. - T1 = insert(-Int, TID, T), - State#state{timings=T1}; - false -> - State - end; -record_timing(_Data, State) -> +handle_cancel(_test_or_group, _data, State) -> State. -print_progress_success(St) -> - print_colored(".", ?GREEN, St). - -print_progress_skipped(St) -> - print_colored("*", ?YELLOW, St). - -print_progress_failed(_Exc, St) -> - print_colored("F", ?RED, St). - -merge_on_end(Data, Dict) -> - ID = proplists:get_value(id, Data), - dict:update(ID, - fun(Old) -> - orddict:merge(fun merge_data/3, Old, orddict:from_list(Data)) - end, Dict). - -merge_data(_K, undefined, X) -> X; -merge_data(_K, X, undefined) -> X; -merge_data(_K, _, X) -> X. - -%%------------------------------------------ -%% Print information at end of run -%%------------------------------------------ -print_failures(#state{failures=[]}) -> - ok; -print_failures(#state{failures=Fails}=State) -> - io:nl(), - io:fwrite("Failures:~n",[]), - lists:foldr(print_failure_fun(State), 1, Fails), - ok. - -print_failure_fun(#state{status=Status}=State) -> - fun(Key, Count) -> - TestData = dict:fetch(Key, Status), - TestId = format_test_identifier(TestData), - io:fwrite("~n ~p) ~ts~n", [Count, TestId]), - print_failure_reason(proplists:get_value(status, TestData), - proplists:get_value(output, TestData), - State), - io:nl(), - Count + 1 - end. - -print_gleam_location(#{function := Function, line := Line, module := Module }, State) -> - X = indent(5, "location: ~s.~s:~p~n", [Module, Function, Line]), - print_colored(X, ?CYAN, State); -print_gleam_location(_, _) -> - ok. - -inspect(X) -> - gleam@string:inspect(X). - -print_gleam_failure_reason( - #{gleam_error := let_assert, message := Message, value := Value}, - State -) -> - print_colored(indent(5, "~s~n", [Message]), ?RED, State), - print_colored(indent(5, " value: ", []), ?RED, State), - print_colored(indent(0, "~ts~n", [inspect(Value)]), ?RESET, State); -print_gleam_failure_reason( - #{gleam_error := todo, message := Message}, - State -) -> - print_colored(indent(5, "todo expression run~n", []), ?RED, State), - print_colored(indent(5, " message: ", []), ?RED, State), - print_colored(indent(0, "~s~n", [Message]), ?RESET, State); -print_gleam_failure_reason(Error, State) -> - print_colored(indent(5, "~p~n", [Error]), ?RED, State). - -% New Gleeunit specific formatters -print_failure_reason( - {error, {error, #{gleam_error := _} = Error, Stack}}, Output, State -) when is_list(Stack) -> - print_gleam_failure_reason(Error, State), - print_gleam_location(Error, State), - print_stack(Stack, State), - print_failure_output(5, Output, State); -print_failure_reason({error, {error, {case_clause, Value}, Stack}}, Output, State) when is_list(Stack) -> - print_colored(indent(5, "No case clause matched~n", []), ?RED, State), - print_colored(indent(5, "Value: ", []), ?CYAN, State), - print_colored(indent(0, "~ts~n", [inspect(Value)]), ?RESET, State), - print_stack(Stack, State), - print_failure_output(5, Output, State); -% From the original Erlang version -print_failure_reason({skipped, Reason}, _Output, State) -> - print_colored(io_lib:format(" ~ts~n", [format_pending_reason(Reason)]), - ?RED, State); -print_failure_reason({error, {_Class, Term, _}}, Output, State) when - is_tuple(Term), tuple_size(Term) == 2, is_list(element(2, Term)) -> - print_assertion_failure(Term, State), - print_failure_output(5, Output, State); -print_failure_reason({error, {error, Error, Stack}}, Output, State) when is_list(Stack) -> - print_colored(indent(5, "Failure: ~p~n", [Error]), ?RED, State), - print_stack(Stack, State), - print_failure_output(5, Output, State); -print_failure_reason({error, Reason}, Output, State) -> - print_colored(indent(5, "Failure: ~p~n", [Reason]), ?RED, State), - print_failure_output(5, Output, State). - -gleam_format_module_name(Module) -> - string:replace(atom_to_list(Module), "@", "/", all). - -print_stack(Stack, State) -> - print_colored(indent(5, "stacktrace:~n", []), ?CYAN, State), - print_stackframes(Stack, State). -print_stackframes([{eunit_test, _, _, _} | Stack], State) -> - print_stackframes(Stack, State); -print_stackframes([{eunit_proc, _, _, _} | Stack], State) -> - print_stackframes(Stack, State); -print_stackframes([{Module, Function, _Arity, _Location} | Stack], State) -> - GleamModule = gleam_format_module_name(Module), - print_colored(indent(7, "~s.~p~n", [GleamModule, Function]), ?CYAN, State), - print_stackframes(Stack, State); -print_stackframes([], _State) -> - ok. - - -print_failure_output(_, <<>>, _) -> ok; -print_failure_output(_, undefined, _) -> ok; -print_failure_output(Indent, Output, State) -> - print_colored(indent(Indent, "output: ~ts", [Output]), ?CYAN, State). - -print_assertion_failure({Type, Props}, State) -> - FailureDesc = format_assertion_failure(Type, Props, 5), - print_colored(FailureDesc, ?RED, State), - io:nl(). - -print_pending(#state{skips=[]}) -> +terminate({ok, _Data}, State) -> + ?reporting:finished(State), ok; -print_pending(#state{status=Status, skips=Skips}=State) -> - io:nl(), - io:fwrite("Pending:~n", []), - lists:foreach(fun(ID) -> - Info = dict:fetch(ID, Status), - case proplists:get_value(reason, Info) of - undefined -> - ok; - Reason -> - print_pending_reason(Reason, Info, State) - end - end, lists:reverse(Skips)), - io:nl(). - -print_pending_reason(Reason0, Data, State) -> - Text = case proplists:get_value(type, Data) of - group -> - io_lib:format(" ~ts~n", [proplists:get_value(desc, Data)]); - test -> - io_lib:format(" ~ts~n", [format_test_identifier(Data)]) - end, - Reason = io_lib:format(" %% ~ts~n", [format_pending_reason(Reason0)]), - print_colored(Text, ?YELLOW, State), - print_colored(Reason, ?CYAN, State). - -print_profile(#state{timings=T, status=Status, profile=true}=State) -> - TopN = take(10, T), - TopNTime = abs(lists:sum([ Time || {Time, _} <- TopN ])), - TLG = dict:fetch([], Status), - TotalTime = proplists:get_value(time, TLG), - if TotalTime =/= undefined andalso TotalTime > 0 andalso TopN =/= [] -> - TopNPct = (TopNTime / TotalTime) * 100, - io:nl(), io:nl(), - io:fwrite("Top ~p slowest tests (~ts, ~.1f% of total time):", [length(TopN), format_time(TopNTime), TopNPct]), - lists:foreach(print_timing_fun(State), TopN), - io:nl(); - true -> ok - end; -print_profile(#state{profile=false}) -> - ok. +terminate({error, Reason}, State) -> + ?reporting:finished(State), + io:fwrite(" +Eunit failed: -print_timing(#state{status=Status}) -> - TLG = dict:fetch([], Status), - Time = proplists:get_value(time, TLG), - io:nl(), - io:fwrite("Finished in ~ts~n", [format_time(Time)]), - ok. +~80p -print_results(Data, State) -> - Pass = proplists:get_value(pass, Data, 0), - Fail = proplists:get_value(fail, Data, 0), - Skip = proplists:get_value(skip, Data, 0), - Cancel = proplists:get_value(cancel, Data, 0), - Total = Pass + Fail + Skip + Cancel, - {Color, Result} = if Fail > 0 -> {?RED, error}; - Skip > 0; Cancel > 0 -> {?YELLOW, error}; - Pass =:= 0 -> {?YELLOW, ok}; - true -> {?GREEN, ok} - end, - print_results(Color, Total, Fail, Skip, Cancel, State), - sync_end(Result). - -print_results(Color, 0, _, _, _, State) -> - print_colored("0 tests\n", Color, State); -print_results(Color, Total, Fail, Skip, Cancel, State) -> - SkipText = format_optional_result(Skip, "skipped"), - CancelText = format_optional_result(Cancel, "cancelled"), - Text = io_lib:format("~p tests, ~p failures~ts~ts~n", [Total, Fail, SkipText, CancelText]), - print_colored(Text, Color, State). +This is probably a bug in gleeunit. Please report it. +", [Reason]), + sync_end(error). -print_timing_fun(#state{status=Status}=State) -> - fun({Time, Key}) -> - TestData = dict:fetch(Key, Status), - TestId = format_test_identifier(TestData), - io:nl(), - io:fwrite(" ~ts~n", [TestId]), - print_colored([" "|format_time(abs(Time))], ?CYAN, State) +sync_end(Result) -> + receive + {stop, Reference, ReplyTo} -> + ReplyTo ! {result, Reference, Result}, + ok end. - -%%------------------------------------------ -%% Print to the console with the given color -%% if enabled. -%%------------------------------------------ -print_colored(Text, Color, #state{colored=true}) -> - io:fwrite("~s~ts~s", [Color, Text, ?RESET]); -print_colored(Text, _Color, #state{colored=false}) -> - io:fwrite("~ts", [Text]). - -%%------------------------------------------ -%% Generic data formatters -%%------------------------------------------ -format_function_name(M, F) -> - M1 = gleam_format_module_name(M), - io_lib:format("~ts.~ts", [M1, F]). - -format_optional_result(0, _) -> - []; -format_optional_result(Count, Text) -> - io_lib:format(", ~p ~ts", [Count, Text]). - -format_test_identifier(Data) -> - {Mod, Fun, _} = proplists:get_value(source, Data), - Line = case proplists:get_value(line, Data) of - 0 -> ""; - L -> io_lib:format(":~p", [L]) - end, - Desc = case proplists:get_value(desc, Data) of - undefined -> ""; - DescText -> io_lib:format(": ~ts", [DescText]) - end, - io_lib:format("~ts~ts~ts", [format_function_name(Mod, Fun), Line, Desc]). - -format_time(undefined) -> - "? seconds"; -format_time(Time) -> - io_lib:format("~.3f seconds", [Time / 1000]). - -format_pending_reason({module_not_found, M}) -> - M1 = gleam_format_module_name(M), - io_lib:format("Module '~ts' missing", [M1]); -format_pending_reason({no_such_function, {M,F,_}}) -> - M1 = gleam_format_module_name(M), - io_lib:format("Function ~ts undefined", [format_function_name(M1,F)]); -format_pending_reason({exit, Reason}) -> - io_lib:format("Related process exited with reason: ~p", [Reason]); -format_pending_reason(Reason) -> - io_lib:format("Unknown error: ~p", [Reason]). - -%% @doc Formats all the known eunit assertions, you're on your own if -%% you make an assertion yourself. -format_assertion_failure(Type, Props, I) when Type =:= assertion_failed - ; Type =:= assert -> - Keys = proplists:get_keys(Props), - HasEUnitProps = ([expression, value] -- Keys) =:= [], - HasHamcrestProps = ([expected, actual, matcher] -- Keys) =:= [], - if - HasEUnitProps -> - [indent(I, "Failure: ?assert(~ts)~n", [proplists:get_value(expression, Props)]), - indent(I, " expected: true~n", []), - case proplists:get_value(value, Props) of - false -> - indent(I, " got: false", []); - {not_a_boolean, V} -> - indent(I, " got: ~p", [V]) - end]; - HasHamcrestProps -> - [indent(I, "Failure: ?assertThat(~p)~n", [proplists:get_value(matcher, Props)]), - indent(I, " expected: ~ts~n", [inspect(proplists:get_value(expected, Props))]), - indent(I, " got: ~ts", [inspect(proplists:get_value(actual, Props))])]; - true -> - [indent(I, "Failure: unknown assert: ~p", [Props])] - end; - -format_assertion_failure(Type, Props, I) when Type =:= assertMatch_failed - ; Type =:= assertMatch -> - Expr = proplists:get_value(expression, Props), - Pattern = proplists:get_value(pattern, Props), - Value = proplists:get_value(value, Props), - [indent(I, "Failure: ?assertMatch(~ts, ~ts)~n", [Pattern, Expr]), - indent(I, " expected: = ~ts~n", [Pattern]), - indent(I, " got: ~p", [Value])]; - -format_assertion_failure(Type, Props, I) when Type =:= assertNotMatch_failed - ; Type =:= assertNotMatch -> - Expr = proplists:get_value(expression, Props), - Pattern = proplists:get_value(pattern, Props), - Value = proplists:get_value(value, Props), - [indent(I, "Failure: ?assertNotMatch(~ts, ~ts)~n", [Pattern, Expr]), - indent(I, " expected not: = ~ts~n", [Pattern]), - indent(I, " got: ~p", [Value])]; - -format_assertion_failure(Type, Props, I) when Type =:= assertEqual_failed - ; Type =:= assertEqual -> - Expected = inspect(proplists:get_value(expected, Props)), - Value = inspect(proplists:get_value(value, Props)), - [indent(I, "Values were not equal~n", []), - indent(I, "expected: ~ts~n", [Expected]), - indent(I, " got: ~ts", [Value])]; - -format_assertion_failure(Type, Props, I) when Type =:= assertNotEqual_failed - ; Type =:= assertNotEqual -> - Value = inspect(proplists:get_value(value, Props)), - [indent(I, "Values were equal~n", []), - indent(I, "expected: not ~ts~n,", [Value]), - indent(I, " got: ~ts", [Value])]; - -format_assertion_failure(Type, Props, I) when Type =:= assertException_failed - ; Type =:= assertException -> - Expr = proplists:get_value(expression, Props), - Pattern = proplists:get_value(pattern, Props), - {Class, Term} = extract_exception_pattern(Pattern), % I hate that we have to do this, why not just give DATA - [indent(I, "Failure: ?assertException(~ts, ~ts, ~ts)~n", [Class, Term, Expr]), - case proplists:is_defined(unexpected_success, Props) of - true -> - [indent(I, " expected: exception ~ts but nothing was raised~n", [Pattern]), - indent(I, " got: value ~p", [proplists:get_value(unexpected_success, Props)])]; - false -> - Ex = proplists:get_value(unexpected_exception, Props), - [indent(I, " expected: exception ~ts~n", [Pattern]), - indent(I, " got: exception ~p", [Ex])] - end]; - -format_assertion_failure(Type, Props, I) when Type =:= assertNotException_failed - ; Type =:= assertNotException -> - Expr = proplists:get_value(expression, Props), - Pattern = proplists:get_value(pattern, Props), - {Class, Term} = extract_exception_pattern(Pattern), % I hate that we have to do this, why not just give DAT - Ex = proplists:get_value(unexpected_exception, Props), - [indent(I, "Failure: ?assertNotException(~ts, ~ts, ~ts)~n", [Class, Term, Expr]), - indent(I, " expected not: exception ~ts~n", [Pattern]), - indent(I, " got: exception ~p", [Ex])]; - -format_assertion_failure(Type, Props, I) when Type =:= command_failed - ; Type =:= command -> - Cmd = proplists:get_value(command, Props), - Expected = proplists:get_value(expected_status, Props), - Status = proplists:get_value(status, Props), - [indent(I, "Failure: ?cmdStatus(~p, ~p)~n", [Expected, Cmd]), - indent(I, " expected: status ~p~n", [Expected]), - indent(I, " got: status ~p", [Status])]; - -format_assertion_failure(Type, Props, I) when Type =:= assertCmd_failed - ; Type =:= assertCmd -> - Cmd = proplists:get_value(command, Props), - Expected = proplists:get_value(expected_status, Props), - Status = proplists:get_value(status, Props), - [indent(I, "Failure: ?assertCmdStatus(~p, ~p)~n", [Expected, Cmd]), - indent(I, " expected: status ~p~n", [Expected]), - indent(I, " got: status ~p", [Status])]; - -format_assertion_failure(Type, Props, I) when Type =:= assertCmdOutput_failed - ; Type =:= assertCmdOutput -> - Cmd = proplists:get_value(command, Props), - Expected = proplists:get_value(expected_output, Props), - Output = proplists:get_value(output, Props), - [indent(I, "Failure: ?assertCmdOutput(~p, ~p)~n", [Expected, Cmd]), - indent(I, " expected: ~p~n", [Expected]), - indent(I, " got: ~p", [Output])]; - -format_assertion_failure(Type, Props, I) -> - indent(I, "~p", [{Type, Props}]). - -indent(I, Fmt, Args) -> - io_lib:format("~" ++ integer_to_list(I) ++ "s" ++ Fmt, [" "|Args]). - -extract_exception_pattern(Str) -> - ["{", Class, Term|_] = re:split(Str, "[, ]{1,2}", [unicode,{return,list}]), - {Class, Term}. diff --git a/test/gleam_panics_test.gleam b/test/gleam_panics_test.gleam index c209240..b81e155 100644 --- a/test/gleam_panics_test.gleam +++ b/test/gleam_panics_test.gleam @@ -10,8 +10,10 @@ import gleeunit/internal/gleam_panic.{ fn rescue(f: fn() -> t) -> Result(t, dynamic.Dynamic) pub fn panic_test() { + panic let assert Error(e) = rescue(fn() { panic }) let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.file == "test/gleam_panics_test.gleam" assert e.kind == Panic assert e.function == "panic_test" assert e.module == "gleam_panics_test" @@ -20,8 +22,10 @@ pub fn panic_test() { } pub fn panic_message_test() { + todo let assert Error(e) = rescue(fn() { panic as "oh my!" }) let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.file == "test/gleam_panics_test.gleam" assert e.kind == Panic assert e.function == "panic_message_test" assert e.module == "gleam_panics_test" @@ -32,6 +36,7 @@ pub fn panic_message_test() { pub fn todo_test() { let assert Error(e) = rescue(fn() { todo }) let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.file == "test/gleam_panics_test.gleam" assert e.kind == Todo assert e.function == "todo_test" assert e.module == "gleam_panics_test" @@ -41,8 +46,11 @@ pub fn todo_test() { } pub fn todo_message_test() { + let get_names = fn() { ["Lucy"] } + assert get_names() == ["Lucy", "Nubi"] let assert Error(e) = rescue(fn() { todo as "oh my!" }) let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.file == "test/gleam_panics_test.gleam" assert e.kind == Todo assert e.function == "todo_message_test" assert e.module == "gleam_panics_test" @@ -51,6 +59,7 @@ pub fn todo_message_test() { } pub fn let_assert_test() { + let assert 1 = 2 let assert Error(e) = rescue(fn() { let assert 0 = function.identity(123) @@ -73,6 +82,7 @@ pub fn let_assert_message_test() { let assert 0 = function.identity(321) as "oh dear" }) let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.file == "test/gleam_panics_test.gleam" assert e.function == "let_assert_message_test" assert e.module == "gleam_panics_test" assert e.line > 1 @@ -91,6 +101,7 @@ pub fn assert_expression_test() { assert x }) let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.file == "test/gleam_panics_test.gleam" assert e.function == "assert_expression_test" assert e.module == "gleam_panics_test" assert e.line > 1 @@ -112,6 +123,7 @@ pub fn assert_expression_message_test() { assert x as "maybe?" }) let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.file == "test/gleam_panics_test.gleam" assert e.function == "assert_expression_message_test" assert e.module == "gleam_panics_test" assert e.line > 1 @@ -132,6 +144,7 @@ pub fn assert_function_test() { assert function.identity(False) }) let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.file == "test/gleam_panics_test.gleam" assert e.function == "assert_function_test" assert e.module == "gleam_panics_test" assert e.line > 1 @@ -152,6 +165,7 @@ pub fn assert_function_message_test() { assert function.identity(False) as "oh!" }) let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.file == "test/gleam_panics_test.gleam" assert e.function == "assert_function_message_test" assert e.module == "gleam_panics_test" assert e.line > 1 @@ -173,6 +187,7 @@ pub fn assert_binary_operator_test() { assert a && function.identity(False) }) let assert Ok(e) = gleam_panic.from_dynamic(e) + assert e.file == "test/gleam_panics_test.gleam" assert e.function == "assert_binary_operator_test" assert e.module == "gleam_panics_test" assert e.line > 1 From 0055910aead3da16e529bdb0e403893b32f3db2f Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Tue, 27 May 2025 17:16:23 +0100 Subject: [PATCH 03/11] Improved test output --- CHANGELOG.md | 5 ++ gleam.toml | 3 ++ manifest.toml | 2 + src/gleeunit/internal/gleam_panic.gleam | 8 +-- src/gleeunit/internal/gleam_panic_ffi.erl | 9 ++-- src/gleeunit/internal/gleam_panic_ffi.mjs | 3 +- src/gleeunit/internal/reporting.gleam | 58 +++++++++++++--------- src/gleeunit_ffi.mjs | 30 ++++-------- test/gleam_panics_test.gleam | 60 +++++++++++------------ testhelper/gleam.toml | 2 + testhelper/manifest.toml | 11 +++++ testhelper/src/testhelper.gleam | 13 +++++ 12 files changed, 119 insertions(+), 85 deletions(-) create mode 100644 testhelper/gleam.toml create mode 100644 testhelper/manifest.toml create mode 100644 testhelper/src/testhelper.gleam diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0f422..decd8a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v1.4.0 - 2025-04-24 + +- Added support for `assert`. +- The console output format has been improved. + ## v1.3.1 - 2025-04-24 - Fixed printing of `let assert` crashes. diff --git a/gleam.toml b/gleam.toml index d825ae9..fdc78c8 100644 --- a/gleam.toml +++ b/gleam.toml @@ -11,3 +11,6 @@ allow_read = ["gleam.toml", "test", "build"] [dependencies] gleam_stdlib = ">= 0.60.0 and < 2.0.0" + +[dev-dependencies] +testhelper = { "path" = "./testhelper" } diff --git a/manifest.toml b/manifest.toml index a838186..20d10b5 100644 --- a/manifest.toml +++ b/manifest.toml @@ -3,7 +3,9 @@ packages = [ { name = "gleam_stdlib", version = "0.60.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "621D600BB134BC239CB2537630899817B1A42E60A1D46C5E9F3FAE39F88C800B" }, + { name = "testhelper", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "testhelper" }, ] [requirements] gleam_stdlib = { version = ">= 0.60.0 and < 2.0.0" } +testhelper = { path = "./testhelper" } diff --git a/src/gleeunit/internal/gleam_panic.gleam b/src/gleeunit/internal/gleam_panic.gleam index 7765da5..b103793 100644 --- a/src/gleeunit/internal/gleam_panic.gleam +++ b/src/gleeunit/internal/gleam_panic.gleam @@ -16,16 +16,12 @@ pub type PanicKind { Panic LetAssert( start: Int, + end: Int, pattern_start: Int, pattern_end: Int, value: dynamic.Dynamic, ) - Assert( - start: Int, - expression_start: Int, - expression_end: Int, - kind: AssertKind, - ) + Assert(start: Int, end: Int, expression_start: Int, kind: AssertKind) } pub type AssertKind { diff --git a/src/gleeunit/internal/gleam_panic_ffi.erl b/src/gleeunit/internal/gleam_panic_ffi.erl index b159100..ea00c04 100644 --- a/src/gleeunit/internal/gleam_panic_ffi.erl +++ b/src/gleeunit/internal/gleam_panic_ffi.erl @@ -4,18 +4,19 @@ from_dynamic(#{ gleam_error := assert, start := Start, - expression_start := EStart, - expression_end := EEnd + 'end' := End, + expression_start := EStart } = E) -> - wrap(E, {assert, Start, EStart, EEnd, assert_kind(E)}); + wrap(E, {assert, Start, End, EStart, assert_kind(E)}); from_dynamic(#{ gleam_error := let_assert, start := Start, + 'end' := End, pattern_start := PStart, pattern_end := PEnd, value := Value } = E) -> - wrap(E, {let_assert, Start, PStart, PEnd, Value}); + wrap(E, {let_assert, Start, End, PStart, PEnd, Value}); from_dynamic(#{gleam_error := panic} = E) -> wrap(E, panic); from_dynamic(#{gleam_error := todo} = E) -> diff --git a/src/gleeunit/internal/gleam_panic_ffi.mjs b/src/gleeunit/internal/gleam_panic_ffi.mjs index ef2b818..cef7646 100644 --- a/src/gleeunit/internal/gleam_panic_ffi.mjs +++ b/src/gleeunit/internal/gleam_panic_ffi.mjs @@ -30,6 +30,7 @@ export function from_dynamic(error) { if (error.gleam_error === "let_assert") { let kind = new LetAssert( error.start, + error.end, error.pattern_start, error.pattern_end, error.value, @@ -40,8 +41,8 @@ export function from_dynamic(error) { if (error.gleam_error === "assert") { let kind = new Assert( error.start, + error.end, error.expression_start, - error.expression_end, assert_kind(error), ); return wrap(error, kind); diff --git a/src/gleeunit/internal/reporting.gleam b/src/gleeunit/internal/reporting.gleam index 8d6c3ea..a76aae0 100644 --- a/src/gleeunit/internal/reporting.gleam +++ b/src/gleeunit/internal/reporting.gleam @@ -2,6 +2,7 @@ import gleam/bit_array import gleam/dynamic import gleam/int import gleam/io +import gleam/list import gleam/option.{type Option} import gleam/result import gleam/string @@ -99,37 +100,37 @@ fn format_gleam_error( gleam_panic.Panic -> { string.concat([ bold(red("panic")) <> " " <> location <> "\n", - blue(" test") <> ": " <> module <> "." <> function <> "\n", - blue(" info") <> ": " <> error.message <> "\n", + cyan(" test") <> ": " <> module <> "." <> function <> "\n", + cyan(" info") <> ": " <> error.message <> "\n", ]) } gleam_panic.Todo -> { string.concat([ bold(yellow("todo")) <> " " <> location <> "\n", - blue(" test") <> ": " <> module <> "." <> function <> "\n", - blue(" info") <> ": " <> error.message <> "\n", + cyan(" test") <> ": " <> module <> "." <> function <> "\n", + cyan(" info") <> ": " <> error.message <> "\n", ]) } - gleam_panic.Assert(start:, expression_end:, kind:, ..) -> { + gleam_panic.Assert(start:, end:, kind:, ..) -> { string.concat([ bold(red("assert")) <> " " <> location <> "\n", - blue(" test") <> ": " <> module <> "." <> function <> "\n", - code_snippet(src, start, expression_end), + cyan(" test") <> ": " <> module <> "." <> function <> "\n", + code_snippet(src, start, end), assert_info(kind), - blue(" info") <> ": " <> error.message <> "\n", + cyan(" info") <> ": " <> error.message <> "\n", ]) } // TODO: include the whole expression - gleam_panic.LetAssert(start:, pattern_end:, value:, ..) -> { + gleam_panic.LetAssert(start:, end:, value:, ..) -> { string.concat([ bold(red("let assert")) <> " " <> location <> "\n", - blue(" test") <> ": " <> module <> "." <> function <> "\n", - code_snippet(src, start, pattern_end), - blue("value") <> ": " <> string.inspect(value) <> "\n", - blue(" info") <> ": " <> error.message <> "\n", + cyan(" test") <> ": " <> module <> "." <> function <> "\n", + code_snippet(src, start, end), + cyan("value") <> ": " <> string.inspect(value) <> "\n", + cyan(" info") <> ": " <> error.message <> "\n", ]) } } @@ -137,21 +138,32 @@ fn format_gleam_error( fn assert_info(kind: gleam_panic.AssertKind) -> String { case kind { - gleam_panic.BinaryOperator(operator:, left:, right:) -> + gleam_panic.BinaryOperator(left:, right:, ..) -> { string.concat([assert_value(" left", left), assert_value("right", right)]) + } - gleam_panic.FunctionCall(arguments:) -> todo + gleam_panic.FunctionCall(arguments:) -> { + arguments + |> list.index_map(fn(e, i) { + let number = string.pad_start(int.to_string(i), 5, " ") + assert_value(number, e) + }) + |> string.concat + } - gleam_panic.OtherExpression(expression:) -> "" + gleam_panic.OtherExpression(..) -> "" } } fn assert_value(name: String, value: gleam_panic.AssertedExpression) -> String { - case value.kind { - gleam_panic.Expression(value:) -> - blue(name) <> ": " <> string.inspect(value) <> "\n" + cyan(name) <> ": " <> inspect_value(value) <> "\n" +} - gleam_panic.Literal(..) | gleam_panic.Unevaluated -> "" +fn inspect_value(value: gleam_panic.AssertedExpression) -> String { + case value.kind { + gleam_panic.Unevaluated -> grey("unevaluated") + gleam_panic.Literal(..) -> grey("literal") + gleam_panic.Expression(value:) -> string.inspect(value) } } @@ -160,7 +172,7 @@ fn code_snippet(src: Option(BitArray), start: Int, end: Int) -> String { use src <- result.try(option.to_result(src, Nil)) use snippet <- result.try(bit_array.slice(src, start, end - start)) use snippet <- result.try(bit_array.to_string(snippet)) - let snippet = blue(" code") <> ": " <> snippet <> "\n" + let snippet = cyan(" code") <> ": " <> snippet <> "\n" Ok(snippet) } |> result.unwrap("") @@ -175,8 +187,8 @@ fn bold(text: String) -> String { "\u{001b}[1m" <> text <> "\u{001b}[22m" } -fn blue(text: String) -> String { - "\u{001b}[34m" <> text <> "\u{001b}[39m" +fn cyan(text: String) -> String { + "\u{001b}[36m" <> text <> "\u{001b}[39m" } fn yellow(text: String) -> String { diff --git a/src/gleeunit_ffi.mjs b/src/gleeunit_ffi.mjs index 2ead732..a6127ff 100644 --- a/src/gleeunit_ffi.mjs +++ b/src/gleeunit_ffi.mjs @@ -1,5 +1,6 @@ import { readFileSync } from "fs"; import { Ok, Error as GleamError } from "./gleam.mjs"; +import * as reporting from "./gleeunit/internal/reporting.mjs"; export function read_file(path) { try { @@ -25,7 +26,7 @@ async function* gleamFiles(directory) { } async function readRootPackageName() { - let toml = await read_file("gleam.toml", "utf-8"); + let toml = await async_read_file("gleam.toml", "utf-8"); for (let line of toml.split("\n")) { let matches = line.match(/\s*name\s*=\s*"([a-z][a-z0-9_]*)"/); // Match regexp in compiler-cli/src/new.rs in validate_name() if (matches) return matches[1]; @@ -34,8 +35,7 @@ async function readRootPackageName() { } export async function main() { - let passes = 0; - let failures = 0; + let state = reporting.new_state(); let packageName = await readRootPackageName(); let dist = `../${packageName}/`; @@ -47,34 +47,22 @@ export async function main() { if (!fnName.endsWith("_test")) continue; try { await module[fnName](); - write(`\u001b[32m.\u001b[0m`); - passes++; + state = reporting.test_passed(state); } catch (error) { - let moduleName = "\n" + js_path.slice(0, -4); - let line = error.line ? `:${error.line}` : ""; - write(`\n❌ ${moduleName}.${fnName}${line}: ${error}\n`); - failures++; + let moduleName = js_path.slice(0, -4); + state = reporting.test_failed(state, moduleName, fnName, error); } } } - console.log(` -${passes + failures} tests, ${failures} failures`); - exit(failures ? 1 : 0); + const status = reporting.finished(state); + exit(status); } export function crash(message) { throw new Error(message); } -function write(message) { - if (globalThis.Deno) { - Deno.stdout.writeSync(new TextEncoder().encode(message)); - } else { - process.stdout.write(message); - } -} - function exit(code) { if (globalThis.Deno) { Deno.exit(code); @@ -101,7 +89,7 @@ function join_path(a, b) { return a + "/" + b; } -async function read_file(path) { +async function async_read_file(path) { if (globalThis.Deno) { return Deno.readTextFile(path); } else { diff --git a/test/gleam_panics_test.gleam b/test/gleam_panics_test.gleam index b81e155..3f5d54d 100644 --- a/test/gleam_panics_test.gleam +++ b/test/gleam_panics_test.gleam @@ -4,13 +4,13 @@ import gleeunit/internal/gleam_panic.{ Assert, BinaryOperator, Expression, FunctionCall, LetAssert, Literal, OtherExpression, Panic, Todo, Unevaluated, } +import testhelper @external(erlang, "gleeunit_test_ffi", "rescue") @external(javascript, "./gleeunit_test_ffi.mjs", "rescue") fn rescue(f: fn() -> t) -> Result(t, dynamic.Dynamic) pub fn panic_test() { - panic let assert Error(e) = rescue(fn() { panic }) let assert Ok(e) = gleam_panic.from_dynamic(e) assert e.file == "test/gleam_panics_test.gleam" @@ -22,7 +22,6 @@ pub fn panic_test() { } pub fn panic_message_test() { - todo let assert Error(e) = rescue(fn() { panic as "oh my!" }) let assert Ok(e) = gleam_panic.from_dynamic(e) assert e.file == "test/gleam_panics_test.gleam" @@ -34,32 +33,29 @@ pub fn panic_message_test() { } pub fn todo_test() { - let assert Error(e) = rescue(fn() { todo }) + let assert Error(e) = rescue(fn() { testhelper.run_todo() }) let assert Ok(e) = gleam_panic.from_dynamic(e) - assert e.file == "test/gleam_panics_test.gleam" + assert e.file == "src/testhelper.gleam" assert e.kind == Todo - assert e.function == "todo_test" - assert e.module == "gleam_panics_test" + assert e.function == "run_todo" + assert e.module == "testhelper" assert e.line > 1 assert e.message == "`todo` expression evaluated. This code has not yet been implemented." } pub fn todo_message_test() { - let get_names = fn() { ["Lucy"] } - assert get_names() == ["Lucy", "Nubi"] - let assert Error(e) = rescue(fn() { todo as "oh my!" }) + let assert Error(e) = rescue(fn() { testhelper.run_todo_message("oh my!") }) let assert Ok(e) = gleam_panic.from_dynamic(e) - assert e.file == "test/gleam_panics_test.gleam" + assert e.file == "src/testhelper.gleam" assert e.kind == Todo - assert e.function == "todo_message_test" - assert e.module == "gleam_panics_test" + assert e.function == "run_todo_message" + assert e.module == "testhelper" assert e.line > 1 assert e.message == "oh my!" } pub fn let_assert_test() { - let assert 1 = 2 let assert Error(e) = rescue(fn() { let assert 0 = function.identity(123) @@ -69,11 +65,13 @@ pub fn let_assert_test() { assert e.module == "gleam_panics_test" assert e.line > 1 assert e.message == "Pattern match failed, no pattern matched the value." - let assert LetAssert(value:, start:, pattern_start:, pattern_end:) = e.kind + let assert LetAssert(value:, start:, end:, pattern_start:, pattern_end:) = + e.kind assert value == dynamic.int(123) assert start > 1 assert pattern_start == start + 11 assert pattern_end == pattern_start + 1 + assert end == pattern_end + 25 } pub fn let_assert_message_test() { @@ -87,11 +85,13 @@ pub fn let_assert_message_test() { assert e.module == "gleam_panics_test" assert e.line > 1 assert e.message == "oh dear" - let assert LetAssert(value:, start:, pattern_start:, pattern_end:) = e.kind + let assert LetAssert(value:, start:, end:, pattern_start:, pattern_end:) = + e.kind assert value == dynamic.int(321) assert start > 1 assert pattern_start == start + 11 assert pattern_end == pattern_start + 1 + assert end == pattern_end + 25 } pub fn assert_expression_test() { @@ -106,13 +106,13 @@ pub fn assert_expression_test() { assert e.module == "gleam_panics_test" assert e.line > 1 assert e.message == "Assertion failed." - let assert Assert(start:, expression_start:, expression_end:, kind:) = e.kind + let assert Assert(start:, end:, expression_start:, kind:) = e.kind assert start > 1 assert expression_start == start + 7 - assert expression_end == expression_start + 1 + assert end == expression_start + 1 let assert OtherExpression(expression:) = kind assert expression.start == expression_start - assert expression.end == expression_end + assert expression.end == end assert expression.kind == Expression(value: dynamic.bool(False)) } @@ -128,13 +128,13 @@ pub fn assert_expression_message_test() { assert e.module == "gleam_panics_test" assert e.line > 1 assert e.message == "maybe?" - let assert Assert(start:, expression_start:, expression_end:, kind:) = e.kind + let assert Assert(start:, end:, expression_start:, kind:) = e.kind assert start > 1 assert expression_start == start + 7 - assert expression_end == expression_start + 1 + assert end == expression_start + 1 let assert OtherExpression(expression:) = kind assert expression.start == expression_start - assert expression.end == expression_end + assert expression.end == end assert expression.kind == Expression(value: dynamic.bool(False)) } @@ -149,13 +149,13 @@ pub fn assert_function_test() { assert e.module == "gleam_panics_test" assert e.line > 1 assert e.message == "Assertion failed." - let assert Assert(start:, expression_start:, expression_end:, kind:) = e.kind + let assert Assert(start:, expression_start:, end:, kind:) = e.kind assert start > 1 assert expression_start == start + 7 - assert expression_end == expression_start + 24 + assert end == expression_start + 24 let assert FunctionCall(arguments: [expression]) = kind assert expression.start == expression_start + 18 - assert expression.end == expression_end - 1 + assert expression.end == end - 1 assert expression.kind == Literal(value: dynamic.bool(False)) } @@ -170,13 +170,13 @@ pub fn assert_function_message_test() { assert e.module == "gleam_panics_test" assert e.line > 1 assert e.message == "oh!" - let assert Assert(start:, expression_start:, expression_end:, kind:) = e.kind + let assert Assert(start:, expression_start:, end:, kind:) = e.kind assert start > 1 assert expression_start == start + 7 - assert expression_end == expression_start + 24 + assert end == expression_start + 24 let assert FunctionCall(arguments: [expression]) = kind assert expression.start == expression_start + 18 - assert expression.end == expression_end - 1 + assert expression.end == end - 1 assert expression.kind == Literal(value: dynamic.bool(False)) } @@ -192,10 +192,10 @@ pub fn assert_binary_operator_test() { assert e.module == "gleam_panics_test" assert e.line > 1 assert e.message == "Assertion failed." - let assert Assert(start:, expression_start:, expression_end:, kind:) = e.kind + let assert Assert(start:, expression_start:, end:, kind:) = e.kind assert start > 1 assert expression_start == start + 7 - assert expression_end == expression_start + 29 + assert end == expression_start + 29 let assert BinaryOperator(operator:, left:, right:) = kind assert operator == "&&" assert left.start == expression_start @@ -203,6 +203,6 @@ pub fn assert_binary_operator_test() { assert left.kind == Expression(dynamic.bool(False)) assert right.start == left.end + 4 assert right.end == right.start + 24 - assert right.end == expression_end + assert right.end == end assert right.kind == Unevaluated } diff --git a/testhelper/gleam.toml b/testhelper/gleam.toml new file mode 100644 index 0000000..9b1c2e4 --- /dev/null +++ b/testhelper/gleam.toml @@ -0,0 +1,2 @@ +name = "testhelper" +version = "1.0.0" diff --git a/testhelper/manifest.toml b/testhelper/manifest.toml new file mode 100644 index 0000000..548dbe1 --- /dev/null +++ b/testhelper/manifest.toml @@ -0,0 +1,11 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.60.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "621D600BB134BC239CB2537630899817B1A42E60A1D46C5E9F3FAE39F88C800B" }, + { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/testhelper/src/testhelper.gleam b/testhelper/src/testhelper.gleam new file mode 100644 index 0000000..97e9b5f --- /dev/null +++ b/testhelper/src/testhelper.gleam @@ -0,0 +1,13 @@ +import gleam/io + +pub fn run_todo() -> Nil { + todo +} + +pub fn run_todo_message(message: String) -> Nil { + todo as message +} + +pub fn run_assert(value: Bool) -> Nil { + assert value +} From 988bd04661fd8a14e4cd2cec38857d57730cffa9 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Thu, 29 May 2025 11:19:43 +0100 Subject: [PATCH 04/11] Improve old style assertions --- .github/workflows/test.yml | 2 +- gleam.toml | 2 +- manifest.toml | 2 +- src/gleeunit/should.gleam | 16 ++++------------ testhelper/gleam.toml | 1 + testhelper/manifest.toml | 4 +--- 6 files changed, 9 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 37b6455..3eb239c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: - uses: erlef/setup-beam@v1 with: otp-version: "25.1" - gleam-version: "1.2.0" + gleam-version: "1.11.0" - uses: denoland/setup-deno@v1 with: deno-version: v1.x diff --git a/gleam.toml b/gleam.toml index fdc78c8..a67d04b 100644 --- a/gleam.toml +++ b/gleam.toml @@ -10,7 +10,7 @@ gleam = ">= 1.11.0" allow_read = ["gleam.toml", "test", "build"] [dependencies] -gleam_stdlib = ">= 0.60.0 and < 2.0.0" +gleam_stdlib = ">= 0.60.0 and < 1.0.0" [dev-dependencies] testhelper = { "path" = "./testhelper" } diff --git a/manifest.toml b/manifest.toml index 20d10b5..f73875e 100644 --- a/manifest.toml +++ b/manifest.toml @@ -7,5 +7,5 @@ packages = [ ] [requirements] -gleam_stdlib = { version = ">= 0.60.0 and < 2.0.0" } +gleam_stdlib = { version = ">= 0.60.0 and < 1.0.0" } testhelper = { path = "./testhelper" } diff --git a/src/gleeunit/should.gleam b/src/gleeunit/should.gleam index bdd72f8..11cd82b 100644 --- a/src/gleeunit/should.gleam +++ b/src/gleeunit/should.gleam @@ -1,27 +1,21 @@ -//// A module for testing your Gleam code. The functions found here are -//// compatible with the Erlang eunit test framework. -//// -//// More information on running eunit can be found in [the rebar3 -//// documentation](https://rebar3.org/docs/testing/eunit/). +//// Use the `assert` keyword instead of this module. import gleam/option.{type Option, None, Some} import gleam/string -@external(erlang, "gleeunit_ffi", "should_equal") pub fn equal(a: t, b: t) -> Nil { case a == b { True -> Nil _ -> panic as string.concat([ - "\n\t", + "\n", string.inspect(a), - "\n\tshould equal \n\t", + "\nshould equal\n", string.inspect(b), ]) } } -@external(erlang, "gleeunit_ffi", "should_not_equal") pub fn not_equal(a: t, b: t) -> Nil { case a != b { True -> Nil @@ -29,13 +23,12 @@ pub fn not_equal(a: t, b: t) -> Nil { panic as string.concat([ "\n", string.inspect(a), - "\nshould not equal \n", + "\nshould not equal\n", string.inspect(b), ]) } } -@external(erlang, "gleeunit_ffi", "should_be_ok") pub fn be_ok(a: Result(a, e)) -> a { case a { Ok(value) -> value @@ -43,7 +36,6 @@ pub fn be_ok(a: Result(a, e)) -> a { } } -@external(erlang, "gleeunit_ffi", "should_be_error") pub fn be_error(a: Result(a, e)) -> e { case a { Error(error) -> error diff --git a/testhelper/gleam.toml b/testhelper/gleam.toml index 9b1c2e4..9c01569 100644 --- a/testhelper/gleam.toml +++ b/testhelper/gleam.toml @@ -1,2 +1,3 @@ name = "testhelper" version = "1.0.0" +dependencies = { gleam_stdlib = ">= 0.60.0 and < 1.0.0" } diff --git a/testhelper/manifest.toml b/testhelper/manifest.toml index 548dbe1..5b10d83 100644 --- a/testhelper/manifest.toml +++ b/testhelper/manifest.toml @@ -3,9 +3,7 @@ packages = [ { name = "gleam_stdlib", version = "0.60.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "621D600BB134BC239CB2537630899817B1A42E60A1D46C5E9F3FAE39F88C800B" }, - { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" }, ] [requirements] -gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } -gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +gleam_stdlib = { version = ">= 0.60.0 and < 1.0.0" } From 7271372e595f5427b997b44e590a0f85a7323a2e Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Thu, 29 May 2025 11:20:41 +0100 Subject: [PATCH 05/11] Use RC --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3eb239c..9303f59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: - uses: erlef/setup-beam@v1 with: otp-version: "25.1" - gleam-version: "1.11.0" + gleam-version: "1.11.0-rc2" - uses: denoland/setup-deno@v1 with: deno-version: v1.x From f22acfc263175c3cfa7183ddac5d27230d81eee7 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Thu, 29 May 2025 11:32:38 +0100 Subject: [PATCH 06/11] node prefix --- src/gleeunit_ffi.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gleeunit_ffi.mjs b/src/gleeunit_ffi.mjs index a6127ff..ea7e301 100644 --- a/src/gleeunit_ffi.mjs +++ b/src/gleeunit_ffi.mjs @@ -1,4 +1,4 @@ -import { readFileSync } from "fs"; +import { readFileSync } from "node:fs"; import { Ok, Error as GleamError } from "./gleam.mjs"; import * as reporting from "./gleeunit/internal/reporting.mjs"; @@ -79,7 +79,7 @@ async function read_dir(path) { } return items; } else { - let { readdir } = await import("fs/promises"); + let { readdir } = await import("node:fs/promises"); return readdir(path); } } @@ -93,7 +93,7 @@ async function async_read_file(path) { if (globalThis.Deno) { return Deno.readTextFile(path); } else { - let { readFile } = await import("fs/promises"); + let { readFile } = await import("node:fs/promises"); let contents = await readFile(path); return contents.toString(); } From 54b4d78ad7af089ad9be9dddbf914a4a9bc7ec98 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Thu, 29 May 2025 20:31:51 +0100 Subject: [PATCH 07/11] Soft-deprecate `should` --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index decd8a4..e4e714e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Added support for `assert`. - The console output format has been improved. +- The `gleeunit/should` module has been soft-deprecated in favour of Gleam's + `assert`. In future releases this module will emit a warning when used. ## v1.3.1 - 2025-04-24 From 140b107a85634bb97c0cc4c5f2d0467c3d0ff4b7 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Thu, 29 May 2025 20:32:26 +0100 Subject: [PATCH 08/11] Remove unused --- testhelper/src/testhelper.gleam | 2 -- 1 file changed, 2 deletions(-) diff --git a/testhelper/src/testhelper.gleam b/testhelper/src/testhelper.gleam index 97e9b5f..f3c794a 100644 --- a/testhelper/src/testhelper.gleam +++ b/testhelper/src/testhelper.gleam @@ -1,5 +1,3 @@ -import gleam/io - pub fn run_todo() -> Nil { todo } From fe9e1db903e6b120a43c62b5a8a2da0fb834a9a0 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Thu, 29 May 2025 20:36:36 +0100 Subject: [PATCH 09/11] Namespace Erlang module --- src/gleeunit/internal/gleam_panic.gleam | 4 ++-- .../{gleam_panic_ffi.erl => gleeunit_gleam_panic_ffi.erl} | 2 +- .../{gleam_panic_ffi.mjs => gleeunit_gleam_panic_ffi.mjs} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename src/gleeunit/internal/{gleam_panic_ffi.erl => gleeunit_gleam_panic_ffi.erl} (97%) rename src/gleeunit/internal/{gleam_panic_ffi.mjs => gleeunit_gleam_panic_ffi.mjs} (100%) diff --git a/src/gleeunit/internal/gleam_panic.gleam b/src/gleeunit/internal/gleam_panic.gleam index b103793..6a5d309 100644 --- a/src/gleeunit/internal/gleam_panic.gleam +++ b/src/gleeunit/internal/gleam_panic.gleam @@ -44,6 +44,6 @@ pub type ExpressionKind { Unevaluated } -@external(erlang, "gleam_panic_ffi", "from_dynamic") -@external(javascript, "./gleam_panic_ffi.mjs", "from_dynamic") +@external(erlang, "gleeunit_gleam_panic_ffi", "from_dynamic") +@external(javascript, "./gleeunit_gleam_panic_ffi.mjs", "from_dynamic") pub fn from_dynamic(data: dynamic.Dynamic) -> Result(GleamPanic, Nil) diff --git a/src/gleeunit/internal/gleam_panic_ffi.erl b/src/gleeunit/internal/gleeunit_gleam_panic_ffi.erl similarity index 97% rename from src/gleeunit/internal/gleam_panic_ffi.erl rename to src/gleeunit/internal/gleeunit_gleam_panic_ffi.erl index ea00c04..d78f5e5 100644 --- a/src/gleeunit/internal/gleam_panic_ffi.erl +++ b/src/gleeunit/internal/gleeunit_gleam_panic_ffi.erl @@ -1,4 +1,4 @@ --module(gleam_panic_ffi). +-module(gleeunit_gleam_panic_ffi). -export([from_dynamic/1]). from_dynamic(#{ diff --git a/src/gleeunit/internal/gleam_panic_ffi.mjs b/src/gleeunit/internal/gleeunit_gleam_panic_ffi.mjs similarity index 100% rename from src/gleeunit/internal/gleam_panic_ffi.mjs rename to src/gleeunit/internal/gleeunit_gleam_panic_ffi.mjs From 090c15e16f370bd67180c32131104d8820b1e009 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Thu, 29 May 2025 20:39:23 +0100 Subject: [PATCH 10/11] Remove comment --- src/gleeunit/internal/reporting.gleam | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gleeunit/internal/reporting.gleam b/src/gleeunit/internal/reporting.gleam index a76aae0..facac9a 100644 --- a/src/gleeunit/internal/reporting.gleam +++ b/src/gleeunit/internal/reporting.gleam @@ -123,7 +123,6 @@ fn format_gleam_error( ]) } - // TODO: include the whole expression gleam_panic.LetAssert(start:, end:, value:, ..) -> { string.concat([ bold(red("let assert")) <> " " <> location <> "\n", From 5f3eab21e5f905146028a57830e7ce56133d7192 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Thu, 29 May 2025 20:39:51 +0100 Subject: [PATCH 11/11] Update OTP --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9303f59..0ad491a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 with: - otp-version: "25.1" + otp-version: "28" gleam-version: "1.11.0-rc2" - uses: denoland/setup-deno@v1 with: