Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 37 additions & 28 deletions src/gleeunit/internal/reporting.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,31 @@ import gleam/string
import gleeunit/internal/gleam_panic.{type GleamPanic}

pub type State {
State(passed: Int, failed: Int, skipped: Int)
State(passed: Int, failed: Int, skipped: Int, todos: Int)
}

pub fn new_state() -> State {
State(passed: 0, failed: 0, skipped: 0)
State(passed: 0, failed: 0, skipped: 0, todos: 0)
}

pub fn finished(state: State) -> Int {
case state {
State(passed: 0, failed: 0, skipped: 0) -> {
State(passed: 0, failed: 0, skipped: 0, todos: 0) -> {
io.println("\nNo tests found!")
1
}
State(failed: 0, skipped: 0, ..) -> {
State(failed: 0, skipped: 0, todos: 0, ..) -> {
let message =
"\n" <> int.to_string(state.passed) <> " passed, no failures"
io.println(green(message))
0
}
State(skipped: 0, ..) -> {
let message =
"\n"
<> int.to_string(state.passed)
<> " passed, "
<> int.to_string(state.failed)
<> " failures"
io.println(red(message))
1
}
State(failed: 0, ..) -> {
let message =
"\n"
<> int.to_string(state.passed)
<> " passed, 0 failures, "
<> int.to_string(state.skipped)
<> " skipped"
<> " passed, 0 failures"
<> suffix(state)
io.println(yellow(message))
1
}
Expand All @@ -54,15 +43,26 @@ pub fn finished(state: State) -> Int {
<> int.to_string(state.passed)
<> " passed, "
<> int.to_string(state.failed)
<> " failures, "
<> int.to_string(state.skipped)
<> " skipped"
<> " failures"
<> suffix(state)
io.println(red(message))
1
}
}
}

fn suffix(state: State) -> String {
let todo_part = case state.todos {
0 -> ""
n -> ", " <> int.to_string(n) <> " todo"
}
let skipped_part = case state.skipped {
0 -> ""
n -> ", " <> int.to_string(n) <> " skipped"
}
todo_part <> skipped_part
}

pub fn test_passed(state: State) -> State {
io.print(green("."))
State(..state, passed: state.passed + 1)
Expand All @@ -74,16 +74,25 @@ pub fn test_failed(
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)
case gleam_panic.from_dynamic(error) {
Ok(gleam_panic.GleamPanic(kind: gleam_panic.Todo, ..) as e) -> {
let src = option.from_result(read_file(e.file))
let message = format_gleam_error(e, module, function, src)
io.print("\n" <> message)
State(..state, todos: state.todos + 1)
}
Ok(e) -> {
let src = option.from_result(read_file(e.file))
let message = format_gleam_error(e, module, function, src)
io.print("\n" <> message)
State(..state, failed: state.failed + 1)
}
Error(_) -> {
let message = format_unknown(module, function, error)
io.print("\n" <> message)
State(..state, failed: state.failed + 1)
}
Error(_) -> format_unknown(module, function, error)
}

io.print("\n" <> message)
State(..state, failed: state.failed + 1)
}

pub fn eunit_missing() -> Result(never, Nil) {
Expand Down
43 changes: 41 additions & 2 deletions src/gleeunit_progress.erl
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,47 @@ handle_end(test, Data, State) ->
end.


handle_cancel(_test_or_group, Data, State) ->
?reporting:test_failed(State, <<"gleeunit">>, <<"main">>, Data).
handle_cancel(test, Data, State) ->
case {proplists:get_value(source, Data), proplists:get_value(reason, Data)} of
{{AtomModule, AtomFunction, _Arity}, Reason} when Reason =/= undefined ->
Module = erlang:atom_to_binary(AtomModule),
Function = erlang:atom_to_binary(AtomFunction),
?reporting:test_failed(State, Module, Function, extract_exception(Reason));
{{AtomModule, AtomFunction, _Arity}, undefined} ->
%% Test was cancelled without a reason (e.g. linked process crash).
%% Store test info for when the group cancel arrives with the reason.
Existing = case get(gleeunit_cancelled_tests) of
undefined -> [];
L -> L
end,
put(gleeunit_cancelled_tests, [{AtomModule, AtomFunction} | Existing]),
State;
_ ->
State
end;
handle_cancel(group, Data, State) ->
case proplists:get_value(reason, Data) of
undefined ->
State;
Reason ->
Exception = extract_exception(Reason),
case erase(gleeunit_cancelled_tests) of
undefined ->
?reporting:test_failed(State, <<"gleeunit">>, <<"main">>, Exception);
Tests ->
lists:foldl(fun({AtomModule, AtomFunction}, Acc) ->
Module = erlang:atom_to_binary(AtomModule),
Function = erlang:atom_to_binary(AtomFunction),
?reporting:test_failed(Acc, Module, Function, Exception)
end, State, Tests)
end
end;
handle_cancel(_Other, _Data, State) ->
State.

extract_exception({exit, {Exception, _Stack}}) -> Exception;
extract_exception({abort, Exception}) -> Exception;
extract_exception(Other) -> Other.

terminate({ok, _Data}, State) ->
?reporting:finished(State),
Expand Down
26 changes: 25 additions & 1 deletion test/gleeunit_test_ffi.erl
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
-module(gleeunit_test_ffi).
-export([rescue/1]).
-export([rescue/1, suppress_output/1, handle_cancel_unknown_type/0]).

rescue(F) ->
try
{ok, F()}
catch
_:Error:_ -> {error, Error}
end.

suppress_output(F) ->
OldGL = group_leader(),
Sink = spawn_link(fun Loop() ->
receive
{io_request, From, ReplyAs, _Request} ->
From ! {io_reply, ReplyAs, ok},
Loop();
_ ->
Loop()
end
end),
group_leader(Sink, self()),
try F()
after
group_leader(OldGL, self()),
unlink(Sink),
exit(Sink, normal)
end.

handle_cancel_unknown_type() ->
State = 'gleeunit@internal@reporting':new_state(),
State = gleeunit_progress:handle_cancel(other, [], State),
ok.
21 changes: 21 additions & 0 deletions test/gleeunit_test_ffi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,24 @@ export function rescue(f) {
return new Error(e);
}
}

export function suppress_output(f) {
const saved = {};
if (typeof process === "object" && process.stdout?.write) {
saved.processWrite = process.stdout.write;
process.stdout.write = () => true;
}
if (typeof Deno === "object" && Deno.stdout?.writeSync) {
saved.denoWriteSync = Deno.stdout.writeSync;
Deno.stdout.writeSync = () => 0;
}
const oldLog = console.log;
console.log = () => {};
try {
return f();
} finally {
if (saved.processWrite) process.stdout.write = saved.processWrite;
if (saved.denoWriteSync) Deno.stdout.writeSync = saved.denoWriteSync;
console.log = oldLog;
}
}
54 changes: 54 additions & 0 deletions test/reporting_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import gleam/dynamic
import gleeunit/internal/reporting
import testhelper

@external(erlang, "gleeunit_test_ffi", "rescue")
@external(javascript, "./gleeunit_test_ffi.mjs", "rescue")
fn rescue(f: fn() -> t) -> Result(t, dynamic.Dynamic)

@external(erlang, "gleeunit_test_ffi", "suppress_output")
@external(javascript, "./gleeunit_test_ffi.mjs", "suppress_output")
fn suppress_output(f: fn() -> t) -> t

pub fn todo_counted_as_todo_not_failure_test() {
let state = reporting.new_state()
let assert Error(error) = rescue(fn() { testhelper.run_todo() })
let state =
suppress_output(fn() {
reporting.test_failed(state, "test_module", "my_test", error)
})
assert state.todos == 1
assert state.failed == 0
assert state.passed == 0
}

pub fn panic_still_counted_as_failure_test() {
let state = reporting.new_state()
let assert Error(error) = rescue(fn() { panic as "something broke" })
let state =
suppress_output(fn() {
reporting.test_failed(state, "test_module", "my_test", error)
})
assert state.failed == 1
assert state.todos == 0
assert state.passed == 0
}

pub fn finished_returns_1_when_todos_present_test() {
let state = reporting.State(passed: 5, failed: 0, skipped: 0, todos: 2)
let exit_code = suppress_output(fn() { reporting.finished(state) })
assert exit_code == 1
}

@external(erlang, "gleeunit_test_ffi", "handle_cancel_unknown_type")
fn handle_cancel_unknown_type() -> Nil

pub fn handle_cancel_unknown_type_does_not_crash_test() {
handle_cancel_unknown_type()
}

pub fn finished_returns_0_when_no_todos_or_failures_test() {
let state = reporting.State(passed: 5, failed: 0, skipped: 0, todos: 0)
let exit_code = suppress_output(fn() { reporting.finished(state) })
assert exit_code == 0
}