diff options
author | HJ <thechairman@thechairman.info> | 2024-02-03 15:09:54 -0500 |
---|---|---|
committer | HJ <thechairman@thechairman.info> | 2024-02-03 15:09:54 -0500 |
commit | 96a3c5c179d8d3fff24eb2953e45f8dd15e2714c (patch) | |
tree | 0a67bc0cfeabe51740bb049c61f16f1ac3bdd4ff /aoc2023/build/packages/adglent/src/showtime/internal | |
parent | 547fe03cf43105f46160e2dd9afff21637eaaf47 (diff) | |
download | gleam_aoc-96a3c5c179d8d3fff24eb2953e45f8dd15e2714c.tar.gz gleam_aoc-96a3c5c179d8d3fff24eb2953e45f8dd15e2714c.zip |
cleanup
Diffstat (limited to 'aoc2023/build/packages/adglent/src/showtime/internal')
12 files changed, 1402 insertions, 0 deletions
diff --git a/aoc2023/build/packages/adglent/src/showtime/internal/common/cli.gleam b/aoc2023/build/packages/adglent/src/showtime/internal/common/cli.gleam new file mode 100644 index 0000000..1c03211 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime/internal/common/cli.gleam @@ -0,0 +1,5 @@ +pub type Capture { + Yes + No + Mixed +} diff --git a/aoc2023/build/packages/adglent/src/showtime/internal/common/common_event_handler.gleam b/aoc2023/build/packages/adglent/src/showtime/internal/common/common_event_handler.gleam new file mode 100644 index 0000000..b90af14 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime/internal/common/common_event_handler.gleam @@ -0,0 +1,101 @@ +import gleam/map.{type Map} +import showtime/internal/common/test_suite.{ + type TestEvent, type TestRun, CompletedTestRun, EndTest, EndTestRun, + EndTestSuite, OngoingTestRun, StartTest, StartTestRun, StartTestSuite, +} + +pub type TestState { + NotStarted + Running + Finished(num_modules: Int) +} + +pub type HandlerState { + HandlerState( + test_state: TestState, + num_done: Int, + events: Map(String, Map(String, TestRun)), + ) +} + +// This is the common event-handler (shared between erlang/JS targets) +// The main strategy is to collect the test-results in a map of maps: +// module_name -> +// test_name -> test_result +// It will also keep track of if it is running (i.e. did it receive the EndTestRun) +// so that the caller can determine when to print test-results +pub fn handle_event( + msg: TestEvent, + system_time: fn() -> Int, + state: HandlerState, +) { + let test_state = state.test_state + let num_done = state.num_done + let events = state.events + let #(updated_test_state, updated_num_done, updated_events) = case msg { + StartTestRun -> #(Running, num_done, events) + StartTestSuite(module) -> { + let maybe_module_events = map.get(events, module.name) + let new_events = case maybe_module_events { + Ok(_) -> events + Error(_) -> + events + |> map.insert(module.name, map.new()) + } + #(test_state, num_done, new_events) + } + StartTest(module, test) -> { + let current_time = system_time() + let maybe_module_events = map.get(events, module.name) + let new_events = case maybe_module_events { + Ok(module_events) -> { + let maybe_test_event = map.get(module_events, test.name) + case maybe_test_event { + Error(_) -> + events + |> map.insert( + module.name, + module_events + |> map.insert(test.name, OngoingTestRun(test, current_time)), + ) + Ok(_) -> events + } + } + Error(_) -> events + } + #(test_state, num_done, new_events) + } + EndTest(module, test, result) -> { + let current_time = system_time() + let maybe_module_events = map.get(events, module.name) + let new_events = case maybe_module_events { + Ok(module_events) -> { + let maybe_test_run = + module_events + |> map.get(test.name) + let updated_module_events = case maybe_test_run { + Ok(OngoingTestRun(test_function, started_at)) -> + module_events + |> map.insert( + test.name, + CompletedTestRun( + test_function, + current_time - started_at, + result, + ), + ) + Error(_) -> module_events + } + events + |> map.insert(module.name, updated_module_events) + } + Error(_) -> events + } + #(test_state, num_done, new_events) + } + EndTestSuite(_) -> #(test_state, num_done + 1, events) + EndTestRun(num_modules) -> #(Finished(num_modules), num_done, events) + _ -> #(Running, num_done, events) + } + HandlerState(updated_test_state, updated_num_done, updated_events) +} diff --git a/aoc2023/build/packages/adglent/src/showtime/internal/common/test_result.gleam b/aoc2023/build/packages/adglent/src/showtime/internal/common/test_result.gleam new file mode 100644 index 0000000..a1d6bd9 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime/internal/common/test_result.gleam @@ -0,0 +1,119 @@ +import gleam/dynamic.{type Dynamic} +import gleam/map.{type Map} + +// These are all the types used for test-results +// NOTE: These are heavily used in the erlang/js ffi:s +// so any changes here are likely to break the ffi:s unless +// the corresponding change is introduced there +// +// Futhermore this has some erlang related names that should +// probably be cleaned up, but it is used by both the ffi:s + +// Currently only one reason, but could be possible to support +// more reasons in the future +pub type IgnoreReason { + Ignore +} + +// This is the return value from running the test-function +// or ignored if the test was ignored +pub type TestReturn { + TestFunctionReturn(value: Dynamic, output_buffer: List(String)) + Ignored(reason: IgnoreReason) +} + +// All data about an exception in the test function is captured +// in this type. +// This is also where the data about the assertions will end up (in reason) +pub type Exception { + ErlangException( + class: Class, + reason: Reason, + stacktrace: TraceList, + output_buffer: List(String), + ) +} + +// Alias for the test-result which is either a TestResult (passed test, ignored or a test-definition) +// or an Exception (failed test) +pub type TestResult = + Result(TestReturn, Exception) + +// Reason is either an assert equal (which is if the error was produced by gleeunit should) +// TODO: Add other asserts +// or it is a gleam error meaning that is was produced by showtime should +// TODO: Rename GleamError to ShowtimeError +pub type Reason { + AssertEqual(details: List(ReasonDetail)) + AssertNotEqual(details: List(ReasonDetail)) + AssertMatch(details: List(ReasonDetail)) + GleamError(details: GleamErrorDetail) + GleamAssert(value: Dynamic, line_no: Int) + GenericException(value: Dynamic) +} + +// ReasonDetail is the union-type used in erlang-exceptions where the reason +// is a list of such details +pub type ReasonDetail { + Module(name: String) + ReasonLine(line_no: Int) + Expression(expression: String) + Expected(value: Dynamic) + Value(value: Dynamic) + Pattern(pattern: String) +} + +// Gleam error detail is produced by showtime should and will hold all the information +// about the assertion (both expected and got) +pub type GleamErrorDetail { + LetAssert( + module: String, + function: String, + line_no: Int, + message: String, + value: Dynamic, + ) +} + +// Class is a part of standard erlang exceptions, but also used on js-side +// TODO: Extend to include a JS specific constructor +pub type Class { + ErlangError + Exit + Throw +} + +// The trace list is part of the standard erlang exception, but is also +// emulated on js-side. +// TODO: Maybe we need a js-version that contain some js-specific trace-elements +pub type TraceList { + TraceList(traces: List(Trace)) +} + +// Trace are the elements in the trace list in an erlang exception +// TODO: Maybe add a js-specific trace (since arity is not really a js attribute) +pub type Trace { + Trace(function: String, arity: Arity, extra_info: List(ExtraInfo)) + TraceModule( + module: String, + function: String, + arity: Arity, + extra_info: List(ExtraInfo), + ) +} + +// Extra info holds information about the file and line +// as well as some dynamic data in a map +// This is currently not used in the reporter +pub type ExtraInfo { + ErrorInfo(error_info: Map(Dynamic, Dynamic)) + File(filename: String) + Line(line_no: Int) +} + +// Arity is the erlang type for arity +// Can be either a number, or a list of arguments +pub type Arity { + Num(arity: Int) + ArgList(arg_list: List(Dynamic)) +} diff --git a/aoc2023/build/packages/adglent/src/showtime/internal/common/test_suite.gleam b/aoc2023/build/packages/adglent/src/showtime/internal/common/test_suite.gleam new file mode 100644 index 0000000..eb58d64 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime/internal/common/test_suite.gleam @@ -0,0 +1,63 @@ +import gleam/option.{type Option} +import showtime/internal/common/test_result.{type TestResult} +import showtime/internal/common/cli.{type Capture} + +// The state (and result) of a test function +pub type TestRun { + OngoingTestRun(test_function: TestFunction, started_at: Int) + CompletedTestRun( + test_function: TestFunction, + total_time: Int, + result: TestResult, + ) +} + +// A test module (found by discovery) +pub type TestModule { + TestModule(name: String, path: Option(String)) +} + +// A test function +pub type TestFunction { + TestFunction(name: String) +} + +// A test suite is a test module together with the test functions +// that were collected from that module +pub type TestSuite { + TestSuite(module: TestModule, tests: List(TestFunction)) +} + +// Test event for the event handler +pub type TestEvent { + StartTestRun + StartTestSuite(test_module: TestModule) + StartTest(test_module: TestModule, test_function: TestFunction) + EndTest( + test_module: TestModule, + test_function: TestFunction, + result: TestResult, + ) + EndTestSuite(test_module: TestModule) + EndTestRun(num_modules: Int) +} + +// Interface for the module handler +pub type TestModuleHandler = + fn(TestModule) -> Nil + +// Interface for the event handler +pub type TestEventHandler = + fn(TestEvent) -> Nil + +// Interface for the module collector +pub type ModuleCollector = + fn(TestModuleHandler) -> List(TestModule) + +// Interface for the function collector +pub type TestFunctionCollector = + fn(TestModule) -> TestSuite + +// Interface for the test runner +pub type TestRunner = + fn(TestSuite, TestEventHandler, List(String), Capture) -> Nil diff --git a/aoc2023/build/packages/adglent/src/showtime/internal/erlang/discover.gleam b/aoc2023/build/packages/adglent/src/showtime/internal/erlang/discover.gleam new file mode 100644 index 0000000..ecb752d --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime/internal/erlang/discover.gleam @@ -0,0 +1,167 @@ +@target(erlang) +import gleam/io +@target(erlang) +import gleam/dynamic.{type Dynamic} +@target(erlang) +import gleam/list +@target(erlang) +import gleam/string +@target(erlang) +import gleam/int +@target(erlang) +import gleam/option.{type Option, None, Some} +@target(erlang) +import gleam/erlang/atom.{type Atom} +@target(erlang) +import showtime/internal/common/test_suite.{ + type TestModule, type TestModuleHandler, type TestSuite, TestFunction, + TestModule, TestSuite, +} +import simplifile + +// Module collector for erlang +// Will search the test folder for files ending with _test and notify +// the module handler about each module it finds +@target(erlang) +pub fn collect_modules( + test_module_handler: TestModuleHandler, + only_modules: Option(List(String)), +) -> List(TestModule) { + collect_modules_in_folder("./test", test_module_handler, only_modules) +} + +@target(erlang) +fn collect_modules_in_folder( + path: String, + test_module_handler: TestModuleHandler, + only_modules: Option(List(String)), +) { + let module_prefix = get_module_prefix(path) + let assert Ok(files) = simplifile.read_directory(path) + let test_modules_in_folder = + files + |> list.filter(string.ends_with(_, "_test.gleam")) + |> list.filter_map(fn(test_module_file) { + let module_name = + module_prefix <> { + test_module_file + |> string.replace(".gleam", "") + } + case only_modules { + Some(only_modules_list) -> { + let module_in_list = + only_modules_list + |> list.any(fn(only_module_name) { + only_module_name == module_name + |> string.replace("@", "/") + }) + case module_in_list { + True -> { + let test_module = TestModule(module_name, Some(test_module_file)) + test_module_handler(test_module) + Ok(test_module) + } + + False -> Error(Nil) + } + } + None -> { + let test_module = TestModule(module_name, Some(test_module_file)) + test_module_handler(test_module) + Ok(test_module) + } + } + }) + let test_modules_in_subfolders = + files + |> list.map(fn(filename) { path <> "/" <> filename }) + |> list.filter(fn(file) { simplifile.is_directory(file) }) + |> list.fold( + [], + fn(modules, subfolder) { + modules + |> list.append(collect_modules_in_folder( + subfolder, + test_module_handler, + only_modules, + )) + }, + ) + test_modules_in_folder + |> list.append(test_modules_in_subfolders) +} + +@target(erlang) +fn get_module_prefix(path) { + let path_without_test = + path + |> string.replace("./test", "") + + let path_without_leading_slash = case + string.starts_with(path_without_test, "/") + { + True -> string.drop_left(path_without_test, 1) + False -> path_without_test + } + let module_prefix = + path_without_leading_slash + |> string.replace("/", "@") + case string.length(module_prefix) { + 0 -> module_prefix + _ -> module_prefix <> "@" + } +} + +// Test function collector for erlang +// Uses erlang `apply` to run `module_info` for the test module +// and collects all the exports ending with _test into a `TestSuite` +@target(erlang) +pub fn collect_test_functions(module: TestModule) -> TestSuite { + let test_functions: List(#(Atom, Int)) = + apply( + atom.create_from_string(module.name), + atom.create_from_string("module_info"), + [dynamic.from(atom.create_from_string("exports"))], + ) + |> dynamic.unsafe_coerce() + + let test_functions_filtered = + test_functions + |> list.map(fn(entry) { + let assert #(name, arity) = entry + #( + name + |> atom.to_string(), + arity, + ) + }) + |> list.filter_map(fn(entry) { + let assert #(name, arity) = entry + case string.ends_with(name, "_test") { + True -> + case arity { + 0 -> Ok(name) + _ -> { + io.println( + "WARNING: function \"" <> name <> "\" has arity: " <> int.to_string( + arity, + ) <> " - cannot be used as test (needs to be 0)", + ) + Error("Wrong arity") + } + } + False -> Error("Non matching name") + } + }) + |> list.filter(string.ends_with(_, "_test")) + |> list.map(fn(function_name) { TestFunction(function_name) }) + TestSuite(module, test_functions_filtered) +} + +@target(erlang) +@external(erlang, "erlang", "apply") +fn apply( + module module: Atom, + function function: Atom, + args args: List(Dynamic), +) -> Dynamic diff --git a/aoc2023/build/packages/adglent/src/showtime/internal/erlang/event_handler.gleam b/aoc2023/build/packages/adglent/src/showtime/internal/erlang/event_handler.gleam new file mode 100644 index 0000000..62a9caf --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime/internal/erlang/event_handler.gleam @@ -0,0 +1,91 @@ +@target(erlang) +import gleam/io +@target(erlang) +import gleam/otp/actor.{Continue, Stop} +@target(erlang) +import gleam/erlang/process.{type Subject, Normal} +@target(erlang) +import gleam/map +@target(erlang) +import showtime/internal/common/test_suite.{type TestEvent, EndTestRun} +@target(erlang) +import showtime/internal/common/common_event_handler.{ + Finished, HandlerState, NotStarted, handle_event, +} +@target(erlang) +import showtime/internal/reports/formatter.{create_test_report} +@target(erlang) +import gleam/erlang.{Millisecond} +@target(erlang) +import gleam/option.{None} + +@target(erlang) +type EventHandlerMessage { + EventHandlerMessage(test_event: TestEvent, reply_to: Subject(Int)) +} + +// Starts an actor that receives test events and forwards the to the event handler +// When handler updates the state to `Finished` the actor will wait until handler +// reports that all modules are done and the stop +@target(erlang) +pub fn start() { + let assert Ok(subject) = + actor.start( + #(NotStarted, 0, map.new()), + fn(msg: EventHandlerMessage, state) { + let EventHandlerMessage(test_event, reply_to) = msg + let #(test_state, num_done, events) = state + let updated_state = + handle_event( + test_event, + system_time, + HandlerState(test_state, num_done, events), + ) + case updated_state { + HandlerState(Finished(num_modules), num_done, events) if num_done == num_modules -> { + let #(test_report, num_failed) = create_test_report(events) + io.println(test_report) + process.send(reply_to, num_failed) + Stop(Normal) + } + HandlerState(test_state, num_done, events) -> + Continue(#(test_state, num_done, events), None) + } + }, + ) + let parent_subject = process.new_subject() + + let selector = + process.new_selector() + |> process.selecting(parent_subject, fn(x) { x }) + + // Returns a callback that can receive test events + fn(test_event: TestEvent) { + case test_event { + EndTestRun(..) -> { + // When EndTestRun has been received the callback will wait until the + // actor has stopped + // TODO: Use a timeout? + process.send(subject, EventHandlerMessage(test_event, parent_subject)) + let num_failed = process.select_forever(selector) + case num_failed > 0 { + True -> halt(1) + False -> halt(0) + } + } + + // Normally just send the test event to the actor + _ -> + process.send(subject, EventHandlerMessage(test_event, parent_subject)) + } + } +} + +@target(erlang) +@external(erlang, "erlang", "halt") +fn halt(a: Int) -> Nil + +@target(erlang) +fn system_time() { + erlang.system_time(Millisecond) +} diff --git a/aoc2023/build/packages/adglent/src/showtime/internal/erlang/module_handler.gleam b/aoc2023/build/packages/adglent/src/showtime/internal/erlang/module_handler.gleam new file mode 100644 index 0000000..88cc251 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime/internal/erlang/module_handler.gleam @@ -0,0 +1,43 @@ +@target(erlang) +import gleam/otp/actor.{Continue} +@target(erlang) +import gleam/erlang/process +@target(erlang) +import showtime/internal/common/test_suite.{ + type TestEventHandler, type TestFunctionCollector, type TestModule, + type TestRunner, EndTestSuite, StartTestSuite, +} +@target(erlang) +import showtime/internal/common/cli.{type Capture} +@target(erlang) +import gleam/option.{None} + +@target(erlang) +pub fn start( + test_event_handler: TestEventHandler, + test_function_collector: TestFunctionCollector, + run_test_suite: TestRunner, + ignore_tags: List(String), + capture: Capture, +) { + let assert Ok(subject) = + actor.start( + Nil, + fn(module: TestModule, state) { + process.start( + fn() { + let test_suite = test_function_collector(module) + test_event_handler(StartTestSuite(module)) + run_test_suite(test_suite, test_event_handler, ignore_tags, capture) + test_event_handler(EndTestSuite(module)) + }, + False, + ) + Continue(state, None) + }, + ) + fn(test_module: TestModule) { + process.send(subject, test_module) + Nil + } +} diff --git a/aoc2023/build/packages/adglent/src/showtime/internal/erlang/runner.gleam b/aoc2023/build/packages/adglent/src/showtime/internal/erlang/runner.gleam new file mode 100644 index 0000000..ebbf426 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime/internal/erlang/runner.gleam @@ -0,0 +1,59 @@ +@target(erlang) +import gleam/list +@target(erlang) +import gleam/erlang/atom.{type Atom} +@target(erlang) +import showtime/internal/common/test_suite.{ + type TestEventHandler, type TestSuite, EndTest, StartTest, +} +@target(erlang) +import showtime/internal/common/test_result.{type TestResult} +@target(erlang) +import showtime/internal/common/cli.{type Capture} + +// Runs all tests in a test suite +@target(erlang) +pub fn run_test_suite( + test_suite: TestSuite, + test_event_handler: TestEventHandler, + ignore_tags: List(String), + capture: Capture, +) { + test_suite.tests + |> list.each(fn(test) { + test_event_handler(StartTest(test_suite.module, test)) + let result = + run_test(test_suite.module.name, test.name, ignore_tags, capture) + test_event_handler(EndTest(test_suite.module, test, result)) + }) +} + +// Wrapper around the ffi function that converts names to atoms +@target(erlang) +pub fn run_test( + module_name: String, + test_name: String, + ignore_tags: List(String), + capture: Capture, +) -> TestResult { + let result = + run_test_ffi( + atom.create_from_string(module_name), + atom.create_from_string(test_name), + ignore_tags, + capture, + ) + result +} + +// Calls ffi for running a test function +// The ffi will take care of mapping the result and exception to the data-types +// used in gleam +@target(erlang) +@external(erlang, "showtime_ffi", "run_test") +fn run_test_ffi( + module module: Atom, + function function: Atom, + ignore_tags ignore_tags: List(String), + capture capture: Capture, +) -> TestResult diff --git a/aoc2023/build/packages/adglent/src/showtime/internal/reports/compare.gleam b/aoc2023/build/packages/adglent/src/showtime/internal/reports/compare.gleam new file mode 100644 index 0000000..5ccddee --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime/internal/reports/compare.gleam @@ -0,0 +1,42 @@ +import gleam/dynamic.{type Dynamic} +import gleam/string +import showtime/internal/reports/styles.{expected_highlight, got_highlight} +import gap.{compare_lists, compare_strings} +import gap/styling.{from_comparison, highlight, to_styled_comparison} + +pub fn compare(expected: Dynamic, got: Dynamic) -> #(String, String) { + let expected_as_list = + expected + |> dynamic.list(dynamic.dynamic) + let got_as_list = + got + |> dynamic.list(dynamic.dynamic) + let expected_as_string = + expected + |> dynamic.string() + let got_as_string = + got + |> dynamic.string() + case expected_as_list, got_as_list, expected_as_string, got_as_string { + Ok(expected_list), Ok(got_list), _, _ -> { + let comparison = + compare_lists(expected_list, got_list) + |> from_comparison() + |> highlight(expected_highlight, got_highlight, fn(item) { item }) + |> to_styled_comparison() + #(comparison.first, comparison.second) + } + _, _, Ok(expected_string), Ok(got_string) -> { + let comparison = + compare_strings(expected_string, got_string) + |> from_comparison() + |> highlight(expected_highlight, got_highlight, fn(item) { item }) + |> to_styled_comparison() + #(comparison.first, comparison.second) + } + _, _, _, _ -> #( + expected_highlight(string.inspect(expected)), + got_highlight(string.inspect(got)), + ) + } +} diff --git a/aoc2023/build/packages/adglent/src/showtime/internal/reports/formatter.gleam b/aoc2023/build/packages/adglent/src/showtime/internal/reports/formatter.gleam new file mode 100644 index 0000000..8c1a6ac --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime/internal/reports/formatter.gleam @@ -0,0 +1,480 @@ +import gleam/io +import gleam/int +import gleam/list +import gleam/string +import gleam/option.{type Option, None, Some} +import gleam/map.{type Map} +import gleam/dynamic.{type Dynamic} +import showtime/internal/common/test_result.{ + type GleamErrorDetail, type ReasonDetail, type Trace, AssertEqual, AssertMatch, + AssertNotEqual, Expected, Expression, GenericException, GleamAssert, + GleamError, Ignored, LetAssert, Pattern, Trace, TraceModule, Value, +} +import showtime/internal/common/test_suite.{type TestRun, CompletedTestRun} +import showtime/tests/should.{type Assertion, Eq, Fail, IsError, IsOk, NotEq} +import showtime/internal/reports/styles.{ + error_style, expected_highlight, failed_style, function_style, got_highlight, + heading_style, ignored_style, not_style, passed_style, stacktrace_style, +} +import showtime/internal/reports/compare.{compare} +import showtime/internal/reports/table.{ + AlignLeft, AlignLeftOverflow, AlignRight, Content, Separator, StyledContent, + Table, align_table, to_string, +} +import showtime/tests/meta.{type Meta} + +type GleeUnitAssertionType { + GleeUnitAssertEqual(message: String) + GleeUnitAssertNotEqual(message: String) + GleeUnitAssertMatch(message: String) +} + +type ModuleAndTest { + ModuleAndTestRun(module_name: String, test_run: TestRun) +} + +type UnifiedError { + UnifiedError( + meta: Option(Meta), + reason: String, + message: String, + expected: String, + got: String, + line: Option(Int), + stacktrace: List(Trace), + ) +} + +pub fn create_test_report(test_results: Map(String, Map(String, TestRun))) { + let all_test_runs = + test_results + |> map.values() + |> list.flat_map(map.values) + let failed_test_runs = + test_results + |> map.to_list() + |> list.flat_map(fn(entry) { + let #(module_name, test_module_results) = entry + test_module_results + |> map.values() + |> list.filter_map(fn(test_run) { + case test_run { + CompletedTestRun(_test_function, _, result) -> + case result { + Error(_) -> Ok(ModuleAndTestRun(module_name, test_run)) + Ok(Ignored(_)) -> Error(Nil) + Ok(_) -> Error(Nil) + } + _ -> { + test_run + |> io.debug() + Error(Nil) + } + } + }) + }) + + let ignored_test_runs = + test_results + |> map.to_list() + |> list.flat_map(fn(entry) { + let #(module_name, test_module_results) = entry + test_module_results + |> map.values() + |> list.filter_map(fn(test_run) { + case test_run { + CompletedTestRun(test_function, _, result) -> + case result { + Ok(Ignored(reason)) -> + Ok(#(module_name <> "." <> test_function.name, reason)) + _ -> Error(Nil) + } + _ -> Error(Nil) + } + }) + }) + + let failed_tests_report = + failed_test_runs + |> list.filter_map(fn(module_and_test_run) { + case module_and_test_run.test_run { + CompletedTestRun(test_function, _total_time, result) -> + case result { + Error(exception) -> + case exception.reason { + AssertEqual(reason_details) -> + Ok(format_reason( + erlang_error_to_unified( + reason_details, + GleeUnitAssertEqual("Assert equal"), + exception.stacktrace.traces, + ), + module_and_test_run.module_name, + test_function.name, + exception.output_buffer, + )) + AssertNotEqual(reason_details) -> + Ok(format_reason( + erlang_error_to_unified( + reason_details, + GleeUnitAssertNotEqual("Assert not equal"), + exception.stacktrace.traces, + ), + module_and_test_run.module_name, + test_function.name, + exception.output_buffer, + )) + AssertMatch(reason_details) -> + Ok(format_reason( + erlang_error_to_unified( + reason_details, + GleeUnitAssertMatch("Assert match"), + exception.stacktrace.traces, + ), + module_and_test_run.module_name, + test_function.name, + exception.output_buffer, + )) + GleamError(reason) -> + Ok(format_reason( + gleam_error_to_unified(reason, exception.stacktrace.traces), + module_and_test_run.module_name, + test_function.name, + exception.output_buffer, + )) + // GleamAssert(value) -> Error(Nil) + GleamAssert(value, line_no) -> + Ok(format_reason( + UnifiedError( + None, + "gleam assert", + "Assert failed", + "Patterns should match", + error_style(string.inspect(value)), + Some(line_no), + exception.stacktrace.traces, + ), + module_and_test_run.module_name, + test_function.name, + exception.output_buffer, + )) + GenericException(value) -> + Ok(format_reason( + UnifiedError( + None, + "generic exception", + "Test function threw an exception", + "Exception in test function", + error_style(string.inspect(value)), + None, + exception.stacktrace.traces, + ), + module_and_test_run.module_name, + test_function.name, + exception.output_buffer, + )) + other -> { + io.println("Other: " <> string.inspect(other)) + panic + Error(Nil) + } + } + _ -> Error(Nil) + } + _ -> Error(Nil) + } + }) + |> list.fold([], fn(rows, test_rows) { list.append(rows, test_rows) }) + + let all_test_execution_time_reports = + all_test_runs + |> list.filter_map(fn(test_run) { + case test_run { + CompletedTestRun(test_function, total_time, _) -> + Ok(test_function.name <> ": " <> int.to_string(total_time) <> " ms") + _ -> Error(Nil) + } + }) + let _execution_times_report = + all_test_execution_time_reports + |> string.join("\n") + + let all_tests_count = + all_test_runs + |> list.length() + let ignored_tests_count = + ignored_test_runs + |> list.length() + let failed_tests_count = + failed_test_runs + |> list.length() + + let passed = + passed_style( + int.to_string(all_tests_count - failed_tests_count - ignored_tests_count) <> " passed", + ) + let failed = failed_style(int.to_string(failed_tests_count) <> " failed") + let ignored = case ignored_tests_count { + 0 -> "" + _ -> ", " <> ignored_style(int.to_string(ignored_tests_count) <> " ignored") + } + + let failed_tests_table = + Table(None, failed_tests_report) + |> align_table() + |> to_string() + + let test_report = + "\n" <> failed_tests_table <> "\n" <> passed <> ", " <> failed <> ignored + #(test_report, failed_tests_count) +} + +fn erlang_error_to_unified( + error_details: List(ReasonDetail), + assertion_type: GleeUnitAssertionType, + stacktrace: List(Trace), +) { + error_details + |> list.fold( + UnifiedError( + None, + "not_set", + assertion_type.message, + "", + "", + None, + stacktrace, + ), + fn(unified, reason) { + case reason { + Expression(expression) -> UnifiedError(..unified, reason: expression) + Expected(value) -> + case assertion_type { + GleeUnitAssertEqual(_messaged) -> + UnifiedError( + ..unified, + expected: expected_highlight(string.inspect(value)), + ) + _ -> unified + } + Value(value) -> + case assertion_type { + GleeUnitAssertNotEqual(_message) -> + UnifiedError( + ..unified, + expected: not_style("not ") <> string.inspect(value), + got: got_highlight(string.inspect(value)), + ) + _ -> + UnifiedError(..unified, got: got_highlight(string.inspect(value))) + } + Pattern(pattern) -> + case pattern { + "{ ok , _ }" -> + UnifiedError(..unified, expected: expected_highlight("Ok(_)")) + "{ error , _ }" -> + UnifiedError(..unified, expected: expected_highlight("Error(_)")) + _ -> unified + } + _ -> unified + } + }, + ) +} + +fn gleam_error_to_unified( + gleam_error: GleamErrorDetail, + stacktrace: List(Trace), +) -> UnifiedError { + case gleam_error { + LetAssert(_module, _function, _line_no, _message, value) -> { + let result: Result(Dynamic, Assertion(Dynamic, Dynamic)) = + dynamic.unsafe_coerce(value) + let assert Error(assertion) = result + case assertion { + Eq(got, expected, meta) -> { + let #(expected, got) = compare(expected, got) + UnifiedError( + meta, + "assert", + "Assert equal", + expected, + got, + None, + stacktrace, + ) + } + NotEq(got, expected, meta) -> + UnifiedError( + meta, + "assert", + "Assert not equal", + not_style("not ") <> string.inspect(expected), + string.inspect(got), + None, + stacktrace, + ) + IsOk(got, meta) -> + UnifiedError( + meta, + "assert", + "Assert is Ok", + expected_highlight("Ok(_)"), + got_highlight(string.inspect(got)), + None, + stacktrace, + ) + IsError(got, meta) -> + UnifiedError( + meta, + "assert", + "Assert is Ok", + expected_highlight("Error(_)"), + got_highlight(string.inspect(got)), + None, + stacktrace, + ) + Fail(meta) -> + UnifiedError( + meta, + "assert", + "Assert is Ok", + got_highlight("should.fail()"), + got_highlight("N/A - test always expected to fail"), + None, + stacktrace, + ) + } + } + } +} + +fn format_reason( + error: UnifiedError, + module: String, + function: String, + output_buffer: List(String), +) { + let meta = case error.meta { + Some(meta) -> + Some([ + AlignRight(StyledContent(heading_style("Description")), 2), + Separator(": "), + AlignLeft(Content(meta.description), 0), + ]) + + None -> None + } + + let stacktrace = + error.stacktrace + |> list.map(fn(trace) { + case trace { + Trace(function, _, _) if function == "" -> "(anonymous)" + TraceModule(module, function, _, _) if function == "" -> + module <> "." <> "(anonymous)" + Trace(function, _, _) -> function + TraceModule(module, function, _, _) -> module <> "." <> function + } + }) + let stacktrace_rows = case stacktrace { + [] -> [] + [first, ..rest] -> { + let first_row = + Some([ + AlignRight(StyledContent(heading_style("Stacktrace")), 2), + Separator(": "), + AlignLeft(StyledContent(stacktrace_style(first)), 0), + ]) + let rest_rows = + rest + |> list.map(fn(row) { + Some([ + AlignRight(Content(""), 2), + Separator(" "), + AlignLeft(StyledContent(stacktrace_style(row)), 0), + ]) + }) + [first_row, ..rest_rows] + } + } + + let output_rows = case + output_buffer + |> list.reverse() + |> list.map(fn(row) { string.trim_right(row) }) + { + [] -> [] + [first, ..rest] -> { + let first_row = + Some([ + AlignRight(StyledContent(heading_style("Output")), 2), + Separator(": "), + AlignLeftOverflow(StyledContent(stacktrace_style(first)), 0), + ]) + let rest_rows = + rest + |> list.map(fn(row) { + Some([ + AlignRight(Content(""), 2), + Separator(" "), + AlignLeftOverflow(StyledContent(stacktrace_style(row)), 0), + ]) + }) + [first_row, ..rest_rows] + } + } + + let line = + error.line + |> option.map(fn(line) { ":" <> int.to_string(line) }) + |> option.unwrap("") + + let arrow = + string.join( + list.repeat( + "-", + string.length(module) + 1 + { + string.length(function) + string.length(line) + } / 2, + ), + "", + ) <> "⌄" + let standard_table_rows = [ + Some([ + AlignRight(StyledContent(error_style("Failed")), 2), + Separator(": "), + AlignLeft(Content(arrow), 0), + ]), + Some([ + AlignRight(StyledContent(heading_style("Test")), 2), + Separator(": "), + AlignLeft( + StyledContent(module <> "." <> function_style(function <> line)), + 0, + ), + ]), + meta, + Some([ + AlignRight(StyledContent(heading_style("Expected")), 2), + Separator(": "), + AlignLeftOverflow(StyledContent(error.expected), 0), + ]), + Some([ + AlignRight(StyledContent(heading_style("Got")), 2), + Separator(": "), + AlignLeftOverflow(StyledContent(error.got), 0), + ]), + ] + standard_table_rows + |> list.append(stacktrace_rows) + |> list.append(output_rows) + |> list.append([ + Some([ + AlignRight(Content(""), 0), + AlignRight(Content(""), 0), + AlignRight(Content(""), 0), + ]), + ]) + |> list.filter_map(fn(row) { option.to_result(row, Nil) }) +} diff --git a/aoc2023/build/packages/adglent/src/showtime/internal/reports/styles.gleam b/aoc2023/build/packages/adglent/src/showtime/internal/reports/styles.gleam new file mode 100644 index 0000000..b051dd3 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime/internal/reports/styles.gleam @@ -0,0 +1,84 @@ +import gleam_community/ansi +import gleam/list +import gleam/string +import gleam/bit_array + +pub fn passed_style(text) { + bold_green(text) +} + +pub fn failed_style(text) { + bold_red(text) +} + +pub fn ignored_style(text) { + bold_yellow(text) +} + +pub fn error_style(text) { + bold_red(text) +} + +pub fn expected_highlight(text) { + bold_green(text) +} + +pub fn got_highlight(text) { + bold_red(text) +} + +pub fn not_style(text) { + ansi.bold(text) +} + +pub fn module_style(text: String) { + ansi.cyan(text) +} + +pub fn heading_style(text: String) { + ansi.cyan(text) +} + +pub fn function_style(text: String) { + bold_cyan(text) +} + +pub fn stacktrace_style(text: String) { + text +} + +fn bold_red(text: String) { + ansi.bold(ansi.red(text)) +} + +fn bold_green(text) { + ansi.bold(ansi.green(text)) +} + +fn bold_yellow(text) { + ansi.bold(ansi.yellow(text)) +} + +fn bold_cyan(text) { + ansi.bold(ansi.cyan(text)) +} + +pub fn strip_style(text) { + let #(new_text, _) = + text + |> string.to_graphemes() + |> list.fold( + #("", False), + fn(acc, char) { + let #(str, removing) = acc + let bit_char = bit_array.from_string(char) + case bit_char, removing { + <<0x1b>>, _ -> #(str, True) + <<0x6d>>, True -> #(str, False) + _, True -> #(str, True) + _, False -> #(str <> char, False) + } + }, + ) + new_text +} diff --git a/aoc2023/build/packages/adglent/src/showtime/internal/reports/table.gleam b/aoc2023/build/packages/adglent/src/showtime/internal/reports/table.gleam new file mode 100644 index 0000000..f8bc00c --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime/internal/reports/table.gleam @@ -0,0 +1,148 @@ +import gleam/list +import gleam/string +import gleam/int +import gleam/option.{type Option} +import showtime/internal/reports/styles.{strip_style} + +pub type Content { + Content(unstyled_text: String) + StyledContent(styled_text: String) +} + +pub type Col { + AlignRight(content: Content, margin: Int) + AlignLeft(content: Content, margin: Int) + AlignRightOverflow(content: Content, margin: Int) + AlignLeftOverflow(content: Content, margin: Int) + Separator(char: String) + Aligned(content: String) +} + +pub type Table { + Table(header: Option(String), rows: List(List(Col))) +} + +pub fn to_string(table: Table) -> String { + let rows = + table.rows + |> list.map(fn(row) { + row + |> list.filter_map(fn(col) { + case col { + Separator(char) -> Ok(char) + Aligned(content) -> Ok(content) + _ -> Error(Nil) + } + }) + |> string.join("") + }) + |> string.join("\n") + let header = + table.header + |> option.map(fn(header) { header <> "\n" }) + |> option.unwrap("") + header <> rows +} + +pub fn align_table(table: Table) -> Table { + let cols = + table.rows + |> list.transpose() + let col_width = + cols + |> list.map(fn(col) { + col + |> list.map(fn(content) { + case content { + AlignRight(Content(unstyled), _) -> unstyled + AlignRight(StyledContent(styled), _) -> strip_style(styled) + AlignLeft(Content(unstyled), _) -> unstyled + AlignLeft(StyledContent(styled), _) -> strip_style(styled) + AlignLeftOverflow(_, _) -> "" + AlignRightOverflow(_, _) -> "" + Separator(char) -> char + Aligned(content) -> content + } + }) + |> list.fold(0, fn(max, str) { int.max(max, string.length(str)) }) + }) + let aligned_col = + cols + |> list.zip(col_width) + |> list.map(fn(col_and_width) { + let #(col, width) = col_and_width + col + |> list.map(fn(content) { + case content { + AlignRight(Content(unstyled), margin) -> + Aligned(pad_left( + unstyled, + width + margin - string.length(unstyled), + " ", + )) + AlignRight(StyledContent(styled), margin) -> + Aligned(pad_left( + styled, + width + margin - string.length(strip_style(styled)), + " ", + )) + AlignRightOverflow(Content(unstyled), margin) -> + Aligned(pad_left( + unstyled, + width + margin - string.length(unstyled), + " ", + )) + AlignRightOverflow(StyledContent(styled), margin) -> + Aligned(pad_left( + styled, + width + margin - string.length(strip_style(styled)), + " ", + )) + AlignLeft(Content(unstyled), margin) -> + Aligned(pad_right( + unstyled, + width + margin - string.length(unstyled), + " ", + )) + AlignLeft(StyledContent(styled), margin) -> + Aligned(pad_right( + styled, + width + margin - string.length(strip_style(styled)), + " ", + )) + AlignLeftOverflow(Content(unstyled), margin) -> + Aligned(pad_right( + unstyled, + width + margin - string.length(unstyled), + " ", + )) + AlignLeftOverflow(StyledContent(styled), margin) -> + Aligned(pad_right( + styled, + width + margin - string.length(strip_style(styled)), + " ", + )) + Separator(char) -> Separator(char) + Aligned(content) -> Aligned(content) + } + }) + }) + let aligned_rows = + aligned_col + |> list.transpose() + Table(..table, rows: aligned_rows) +} + +fn pad_left(str: String, num: Int, char: String) { + let padding = + list.repeat(char, num) + |> string.join("") + padding <> str +} + +fn pad_right(str: String, num: Int, char: String) { + let padding = + list.repeat(char, num) + |> string.join("") + str <> padding +} |