aboutsummaryrefslogtreecommitdiff
path: root/aoc2023/build/packages/adglent/src/showtime
diff options
context:
space:
mode:
Diffstat (limited to 'aoc2023/build/packages/adglent/src/showtime')
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/internal/common/cli.gleam5
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/internal/common/common_event_handler.gleam101
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/internal/common/test_result.gleam119
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/internal/common/test_suite.gleam63
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/internal/erlang/discover.gleam167
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/internal/erlang/event_handler.gleam91
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/internal/erlang/module_handler.gleam43
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/internal/erlang/runner.gleam59
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/internal/reports/compare.gleam42
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/internal/reports/formatter.gleam480
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/internal/reports/styles.gleam84
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/internal/reports/table.gleam148
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/tests/meta.gleam3
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/tests/should.gleam113
-rw-r--r--aoc2023/build/packages/adglent/src/showtime/tests/test.gleam57
15 files changed, 1575 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
+}
diff --git a/aoc2023/build/packages/adglent/src/showtime/tests/meta.gleam b/aoc2023/build/packages/adglent/src/showtime/tests/meta.gleam
new file mode 100644
index 0000000..cbba414
--- /dev/null
+++ b/aoc2023/build/packages/adglent/src/showtime/tests/meta.gleam
@@ -0,0 +1,3 @@
+pub type Meta {
+ Meta(description: String, tags: List(String))
+}
diff --git a/aoc2023/build/packages/adglent/src/showtime/tests/should.gleam b/aoc2023/build/packages/adglent/src/showtime/tests/should.gleam
new file mode 100644
index 0000000..71578c7
--- /dev/null
+++ b/aoc2023/build/packages/adglent/src/showtime/tests/should.gleam
@@ -0,0 +1,113 @@
+import gleam/option.{type Option, None, Some}
+import showtime/tests/meta.{type Meta}
+
+pub type Assertion(t, e) {
+ Eq(a: t, b: t, meta: Option(Meta))
+ NotEq(a: t, b: t, meta: Option(Meta))
+ IsOk(a: Result(t, e), meta: Option(Meta))
+ IsError(a: Result(t, e), meta: Option(Meta))
+ Fail(meta: Option(Meta))
+}
+
+pub fn equal(a: t, b: t) {
+ evaluate(Eq(a, b, None))
+}
+
+pub fn equal_meta(a: t, b: t, meta: Meta) {
+ evaluate(Eq(a, b, Some(meta)))
+}
+
+pub fn not_equal(a: t, b: t) {
+ evaluate(NotEq(a, b, None))
+}
+
+pub fn not_equal_meta(a: t, b: t, meta: Meta) {
+ evaluate(NotEq(a, b, Some(meta)))
+}
+
+pub fn be_ok(a: Result(o, e)) {
+ evaluate(IsOk(a, None))
+ let assert Ok(value) = a
+ value
+}
+
+pub fn be_ok_meta(a: Result(o, e), meta: Meta) {
+ evaluate(IsOk(a, Some(meta)))
+}
+
+pub fn be_error(a: Result(o, e)) {
+ evaluate(IsError(a, None))
+ let assert Error(value) = a
+ value
+}
+
+pub fn be_error_meta(a: Result(o, e), meta: Meta) {
+ evaluate(IsError(a, Some(meta)))
+}
+
+pub fn fail() {
+ evaluate(Fail(None))
+}
+
+pub fn fail_meta(meta: Meta) {
+ evaluate(Fail(Some(meta)))
+}
+
+pub fn be_true(a: Bool) {
+ a
+ |> equal(True)
+}
+
+pub fn be_true_meta(a: Bool, meta: Meta) {
+ a
+ |> equal_meta(True, meta)
+}
+
+pub fn be_false(a: Bool) {
+ a
+ |> equal(False)
+}
+
+pub fn be_false_meta(a: Bool, meta: Meta) {
+ a
+ |> equal_meta(False, meta)
+}
+
+@external(erlang, "showtime_ffi", "gleam_error")
+fn gleam_error(value: Result(Nil, Assertion(a, b))) -> Nil
+
+pub fn evaluate(assertion) -> Nil {
+ case assertion {
+ Eq(a, b, _meta) ->
+ case a == b {
+ True -> Nil
+ False -> {
+ gleam_error(Error(assertion))
+ }
+ }
+ NotEq(a, b, _meta) ->
+ case a != b {
+ True -> Nil
+ False -> {
+ gleam_error(Error(assertion))
+ }
+ }
+ IsOk(a, _meta) ->
+ case a {
+ Ok(_) -> Nil
+ Error(_) -> {
+ gleam_error(Error(assertion))
+ }
+ }
+ IsError(a, _meta) ->
+ case a {
+ Error(_) -> Nil
+ Ok(_) -> {
+ gleam_error(Error(assertion))
+ }
+ }
+ Fail(_meta) -> {
+ gleam_error(Error(assertion))
+ }
+ }
+}
diff --git a/aoc2023/build/packages/adglent/src/showtime/tests/test.gleam b/aoc2023/build/packages/adglent/src/showtime/tests/test.gleam
new file mode 100644
index 0000000..730f943
--- /dev/null
+++ b/aoc2023/build/packages/adglent/src/showtime/tests/test.gleam
@@ -0,0 +1,57 @@
+import showtime/tests/should
+import showtime/tests/meta.{type Meta}
+import gleam/io
+
+pub type Test {
+ Test(meta: Meta, test_function: fn() -> Nil)
+}
+
+pub type MetaShould(t) {
+ MetaShould(equal: fn(t, t) -> Nil, not_equal: fn(t, t) -> Nil)
+}
+
+pub fn test(meta: Meta, test_function: fn(Meta) -> Nil) {
+ Test(meta, fn() { test_function(meta) })
+}
+
+pub fn with_meta(meta: Meta, test_function: fn(MetaShould(a)) -> Nil) {
+ Test(
+ meta,
+ fn() {
+ test_function(MetaShould(
+ fn(a, b) { equal(a, b, meta) },
+ fn(a, b) { not_equal(a, b, meta) },
+ ))
+ },
+ )
+}
+
+pub fn equal(a: t, b: t, meta: Meta) {
+ io.debug(a)
+ io.debug(b)
+ should.equal_meta(a, b, meta)
+}
+
+pub fn not_equal(a: t, b: t, meta: Meta) {
+ should.equal_meta(a, b, meta)
+}
+
+pub fn be_ok(a: Result(o, e), meta: Meta) {
+ should.be_ok_meta(a, meta)
+}
+
+pub fn be_error(a: Result(o, e), meta: Meta) {
+ should.be_error_meta(a, meta)
+}
+
+pub fn fail(meta: Meta) {
+ should.fail_meta(meta)
+}
+
+pub fn be_true(a: Bool, meta: Meta) {
+ should.be_true_meta(a, meta)
+}
+
+pub fn be_false(a: Bool, meta: Meta) {
+ should.be_false_meta(a, meta)
+}