aboutsummaryrefslogtreecommitdiff
path: root/aoc2023/build/packages/adglent/src/showtime/internal/erlang
diff options
context:
space:
mode:
authorJ.J <thechairman@thechairman.info>2024-05-30 21:50:02 -0400
committerJ.J <thechairman@thechairman.info>2024-05-30 21:50:02 -0400
commit612fd986ab1e00b6d34dc1937136250e08e89325 (patch)
treea3c93952040c6afdf348b5831619a45db7ba0a2e /aoc2023/build/packages/adglent/src/showtime/internal/erlang
parent231c2b688d1e6cf0846d46e883da30e042a9c6cf (diff)
downloadgleam_aoc-612fd986ab1e00b6d34dc1937136250e08e89325.tar.gz
gleam_aoc-612fd986ab1e00b6d34dc1937136250e08e89325.zip
cleanup
Diffstat (limited to 'aoc2023/build/packages/adglent/src/showtime/internal/erlang')
-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
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