diff options
author | J.J <thechairman@thechairman.info> | 2024-05-30 21:50:02 -0400 |
---|---|---|
committer | J.J <thechairman@thechairman.info> | 2024-05-30 21:50:02 -0400 |
commit | 612fd986ab1e00b6d34dc1937136250e08e89325 (patch) | |
tree | a3c93952040c6afdf348b5831619a45db7ba0a2e /aoc2023/build/packages/adglent/src/showtime/internal/erlang | |
parent | 231c2b688d1e6cf0846d46e883da30e042a9c6cf (diff) | |
download | gleam_aoc-612fd986ab1e00b6d34dc1937136250e08e89325.tar.gz gleam_aoc-612fd986ab1e00b6d34dc1937136250e08e89325.zip |
cleanup
Diffstat (limited to 'aoc2023/build/packages/adglent/src/showtime/internal/erlang')
4 files changed, 360 insertions, 0 deletions
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 |