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 | |
parent | 231c2b688d1e6cf0846d46e883da30e042a9c6cf (diff) | |
download | gleam_aoc-612fd986ab1e00b6d34dc1937136250e08e89325.tar.gz gleam_aoc-612fd986ab1e00b6d34dc1937136250e08e89325.zip |
cleanup
Diffstat (limited to 'aoc2023/build/packages/adglent/src')
59 files changed, 5440 insertions, 0 deletions
diff --git a/aoc2023/build/packages/adglent/src/adglent.app.src b/aoc2023/build/packages/adglent/src/adglent.app.src new file mode 100644 index 0000000..aa44f8b --- /dev/null +++ b/aoc2023/build/packages/adglent/src/adglent.app.src @@ -0,0 +1,45 @@ +{application, adglent, [ + {vsn, "1.2.0"}, + {applications, [gap, + gleam_community_ansi, + gleam_erlang, + gleam_http, + gleam_httpc, + gleam_otp, + gleam_stdlib, + gleeunit, + glint, + simplifile, + snag, + tom]}, + {description, "Advent of code helper - automating setup of tests, solution template and problem input"}, + {modules, [adglent, + adglent@day, + adglent@init, + priv@aoc_client, + priv@errors, + priv@prompt, + priv@template, + priv@templates@solution, + priv@templates@test_main, + priv@templates@testfile_gleeunit, + priv@templates@testfile_showtime, + priv@toml, + showtime, + showtime@internal@common@cli, + showtime@internal@common@common_event_handler, + showtime@internal@common@test_result, + showtime@internal@common@test_suite, + showtime@internal@erlang@discover, + showtime@internal@erlang@event_handler, + showtime@internal@erlang@module_handler, + showtime@internal@erlang@runner, + showtime@internal@reports@compare, + showtime@internal@reports@formatter, + showtime@internal@reports@styles, + showtime@internal@reports@table, + showtime@tests@meta, + showtime@tests@should, + showtime@tests@test]}, + {registered, []} +]}. diff --git a/aoc2023/build/packages/adglent/src/adglent.erl b/aoc2023/build/packages/adglent/src/adglent.erl new file mode 100644 index 0000000..e9df2b7 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/adglent.erl @@ -0,0 +1,55 @@ +-module(adglent). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([inspect/1, get_input/1, get_test_folder/1, start_arguments/0, get_part/0]). +-export_type([example/1, problem/0, charlist/0]). + +-type example(OFF) :: {example, binary(), OFF}. + +-type problem() :: first | second. + +-type charlist() :: any(). + +-spec inspect(any()) -> binary(). +inspect(Value) -> + Inspected_value = gleam@string:inspect(Value), + case begin + _pipe = Inspected_value, + gleam@string:starts_with(_pipe, <<"\""/utf8>>) + end of + true -> + _pipe@1 = Inspected_value, + _pipe@2 = gleam@string:drop_left(_pipe@1, 1), + gleam@string:drop_right(_pipe@2, 1); + + false -> + Inspected_value + end. + +-spec get_input(binary()) -> {ok, binary()} | {error, simplifile:file_error()}. +get_input(Day) -> + simplifile:read( + <<<<"src/day"/utf8, Day/binary>>/binary, "/input.txt"/utf8>> + ). + +-spec get_test_folder(binary()) -> binary(). +get_test_folder(Day) -> + <<"test/day"/utf8, Day/binary>>. + +-spec start_arguments() -> list(binary()). +start_arguments() -> + _pipe = init:get_plain_arguments(), + gleam@list:map(_pipe, fun unicode:characters_to_binary/1). + +-spec get_part() -> {ok, problem()} | {error, nil}. +get_part() -> + case start_arguments() of + [<<"1"/utf8>>] -> + {ok, first}; + + [<<"2"/utf8>>] -> + {ok, second}; + + _ -> + {error, nil} + end. diff --git a/aoc2023/build/packages/adglent/src/adglent.gleam b/aoc2023/build/packages/adglent/src/adglent.gleam new file mode 100644 index 0000000..077d49d --- /dev/null +++ b/aoc2023/build/packages/adglent/src/adglent.gleam @@ -0,0 +1,56 @@ +import simplifile.{type FileError} +import gleam/list +import gleam/string + +pub type Example(a) { + Example(input: String, answer: a) +} + +pub fn inspect(value: a) -> String { + let inspected_value = string.inspect(value) + case + inspected_value + |> string.starts_with("\"") + { + True -> + inspected_value + |> string.drop_left(1) + |> string.drop_right(1) + False -> inspected_value + } +} + +pub fn get_input(day: String) -> Result(String, FileError) { + simplifile.read("src/day" <> day <> "/input.txt") +} + +pub fn get_test_folder(day: String) -> String { + "test/day" <> day +} + +pub type Problem { + First + Second +} + +pub fn get_part() -> Result(Problem, Nil) { + case start_arguments() { + ["1"] -> Ok(First) + ["2"] -> Ok(Second) + _ -> Error(Nil) + } +} + +pub fn start_arguments() -> List(String) { + get_start_arguments() + |> list.map(to_string) +} + +type Charlist + +/// Transform a charlist to a string +@external(erlang, "unicode", "characters_to_binary") +fn to_string(a: Charlist) -> String + +@external(erlang, "init", "get_plain_arguments") +fn get_start_arguments() -> List(Charlist) diff --git a/aoc2023/build/packages/adglent/src/adglent/day.gleam b/aoc2023/build/packages/adglent/src/adglent/day.gleam new file mode 100644 index 0000000..69e4ccc --- /dev/null +++ b/aoc2023/build/packages/adglent/src/adglent/day.gleam @@ -0,0 +1,126 @@ +import adglent +import gleam/string +import gleam/result +import priv/template +import priv/templates/testfile_gleeunit +import priv/templates/testfile_showtime +import priv/templates/solution +import priv/toml +import priv/aoc_client +import priv/errors +import simplifile + +pub fn main() { + let day = + case adglent.start_arguments() { + [day] -> Ok(day) + args -> Error("Expected day - found: " <> string.join(args, ", ")) + } + |> errors.map_error("Error when parsing command args") + |> errors.print_error + |> errors.assert_ok + + let aoc_toml = + simplifile.read("aoc.toml") + |> errors.map_error("Could not read aoc.toml") + |> errors.print_error + |> errors.assert_ok + + let aoc_toml_version = toml.get_int(aoc_toml, ["version"]) + let year = + toml.get_string(aoc_toml, ["year"]) + |> errors.map_error("Could not read \"year\" from aoc.toml") + |> errors.print_error + |> errors.assert_ok + let session = + toml.get_string(aoc_toml, ["session"]) + |> errors.map_error("Could not read \"session\" from aoc.toml") + |> errors.print_error + |> errors.assert_ok + + let showtime = case aoc_toml_version { + Ok(2) -> { + toml.get_bool(aoc_toml, ["showtime"]) + |> errors.map_error("Could not read \"showtime\" from aoc.toml") + |> errors.print_error + |> errors.assert_ok + } + _ -> + toml.get_string(aoc_toml, ["showtime"]) + |> result.map(fn(bool_string) { + case bool_string { + "True" -> True + _ -> False + } + }) + |> errors.map_error("Could not read \"showtime\" from aoc.toml") + |> errors.print_error + |> errors.assert_ok + } + + let test_folder = adglent.get_test_folder(day) + let test_file = test_folder <> "/day" <> day <> "_test.gleam" + + simplifile.create_directory_all(test_folder) + |> errors.map_error("Could not create folder \"" <> test_folder <> "\"") + |> errors.print_error + |> errors.assert_ok + + let testfile_template = case showtime { + True -> testfile_showtime.template + False -> testfile_gleeunit.template + } + + template.render(testfile_template, [#("day", day)]) + |> create_file_if_not_present(test_file) + |> errors.print_result + |> errors.assert_ok + + let solutions_folder = "src/day" <> day + let solution_file = solutions_folder <> "/solve.gleam" + + simplifile.create_directory_all(solutions_folder) + |> errors.map_error("Could not create folder \"" <> solutions_folder <> "\"") + |> errors.print_error + |> errors.assert_ok + + template.render(solution.template, [#("day", day)]) + |> create_file_if_not_present(solution_file) + |> errors.print_result + |> errors.assert_ok + + create_file_if_not_present("input.txt", solutions_folder <> "/.gitignore") + |> errors.print_result + |> errors.assert_ok + + let input = + aoc_client.get_input(year, day, session) + |> errors.map_error("Error when fetching input") + |> errors.print_error + |> errors.assert_ok + + input + |> string.trim + |> create_file_if_not_present(solutions_folder <> "/input.txt") + |> errors.print_result + |> errors.assert_ok +} + +fn create_file_if_not_present( + content: String, + path: String, +) -> Result(String, String) { + case simplifile.is_file(path) { + True -> { + Ok(path <> " already exists - skipped") + } + False -> { + use _ <- result.try( + simplifile.create_file(path) + |> errors.map_messages("Created " <> path, "Could not create " <> path), + ) + simplifile.write(content, to: path) + |> errors.map_messages("Wrote " <> path, "Could not write to " <> path) + } + } +} diff --git a/aoc2023/build/packages/adglent/src/adglent/init.gleam b/aoc2023/build/packages/adglent/src/adglent/init.gleam new file mode 100644 index 0000000..42eb833 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/adglent/init.gleam @@ -0,0 +1,110 @@ +import priv/prompt +import priv/template +import priv/templates/test_main +import priv/errors +import priv/toml +import simplifile +import gleam/string +import gleam/list +import gleam/result +import gleam/bool + +const aoc_toml_template = " +version = {{ version }} +year = \"{{ year }}\" +session = \"{{ session }}\" +showtime = {{ showtime }} +" + +pub fn main() { + let year = prompt.value("Year", "2023", False) + let session = prompt.value("Session Cookie", "", False) + let use_showtime = prompt.confirm("Use showtime", False) + + let aoc_toml_file = "aoc.toml" + let overwrite = case simplifile.create_file(aoc_toml_file) { + Ok(_) -> True + Error(simplifile.Eexist) -> + prompt.confirm("aoc.toml exits - overwrite", False) + _ -> panic as "Could not create aoc.toml" + } + case overwrite { + True -> { + template.render( + aoc_toml_template, + [ + #("version", "2"), + #("year", year), + #("session", session), + #( + "showtime", + bool.to_string(use_showtime) + |> string.lowercase, + ), + ], + ) + |> simplifile.write(to: aoc_toml_file) + |> errors.map_messages( + "aoc.toml - written", + "Error when writing aoc.toml", + ) + } + + False -> Ok("aoc.toml - skipped") + } + |> errors.print_result + + let gleam_toml = + simplifile.read("gleam.toml") + |> errors.map_error("Could not read gleam.toml") + |> errors.print_error + |> errors.assert_ok + + let name = + toml.get_string(gleam_toml, ["name"]) + |> errors.map_error("Could not read \"name\" from gleam.toml") + |> errors.print_error + |> errors.assert_ok + + let test_main_file = "test/" <> name <> "_test.gleam" + + case use_showtime { + True -> { + template.render(test_main.template, []) + |> simplifile.write(to: test_main_file) + |> errors.map_messages( + "Wrote " <> test_main_file, + "Could not write to " <> test_main_file, + ) + } + False -> Ok("Using existing (gleeunit) " <> test_main_file) + } + |> errors.print_result + |> errors.assert_ok + + case simplifile.is_file(".gitignore") { + True -> { + use gitignore <- result.try( + simplifile.read(".gitignore") + |> result.map_error(fn(err) { + "Could not read .gitignore: " <> string.inspect(err) + }), + ) + let aoc_toml_ignored = + string.split(gitignore, "\n") + |> list.find(fn(line) { line == "aoc.toml" }) + case aoc_toml_ignored { + Error(_) -> { + simplifile.append("\naoc.toml", to: ".gitignore") + |> errors.map_messages( + ".gitignore written", + "Error when writing .gitignore", + ) + } + Ok(_) -> Ok(".gitignore - skipped (already configured)") + } + } + False -> Error("Could not find .gitignore") + } + |> errors.print_result +} diff --git a/aoc2023/build/packages/adglent/src/adglent@day.erl b/aoc2023/build/packages/adglent/src/adglent@day.erl new file mode 100644 index 0000000..b80368f --- /dev/null +++ b/aoc2023/build/packages/adglent/src/adglent@day.erl @@ -0,0 +1,278 @@ +-module(adglent@day). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([main/0]). + +-spec create_file_if_not_present(binary(), binary()) -> {ok, binary()} | + {error, binary()}. +create_file_if_not_present(Content, Path) -> + case simplifile:is_file(Path) of + true -> + {ok, <<Path/binary, " already exists - skipped"/utf8>>}; + + false -> + gleam@result:'try'( + begin + _pipe = simplifile:create_file(Path), + priv@errors:map_messages( + _pipe, + <<"Created "/utf8, Path/binary>>, + <<"Could not create "/utf8, Path/binary>> + ) + end, + fun(_) -> _pipe@1 = simplifile:write(Path, Content), + priv@errors:map_messages( + _pipe@1, + <<"Wrote "/utf8, Path/binary>>, + <<"Could not write to "/utf8, Path/binary>> + ) end + ) + end. + +-spec main() -> binary(). +main() -> + Day@1 = begin + _pipe = case adglent:start_arguments() of + [Day] -> + {ok, Day}; + + Args -> + {error, + <<"Expected day - found: "/utf8, + (gleam@string:join(Args, <<", "/utf8>>))/binary>>} + end, + _pipe@1 = priv@errors:map_error( + _pipe, + <<"Error when parsing command args"/utf8>> + ), + _pipe@2 = priv@errors:print_error(_pipe@1), + priv@errors:assert_ok(_pipe@2) + end, + Aoc_toml = begin + _pipe@3 = simplifile:read(<<"aoc.toml"/utf8>>), + _pipe@4 = priv@errors:map_error( + _pipe@3, + <<"Could not read aoc.toml"/utf8>> + ), + _pipe@5 = priv@errors:print_error(_pipe@4), + priv@errors:assert_ok(_pipe@5) + end, + Aoc_toml_version = priv@toml:get_int(Aoc_toml, [<<"version"/utf8>>]), + Year = begin + _pipe@6 = priv@toml:get_string(Aoc_toml, [<<"year"/utf8>>]), + _pipe@7 = priv@errors:map_error( + _pipe@6, + <<"Could not read \"year\" from aoc.toml"/utf8>> + ), + _pipe@8 = priv@errors:print_error(_pipe@7), + priv@errors:assert_ok(_pipe@8) + end, + Session = begin + _pipe@9 = priv@toml:get_string(Aoc_toml, [<<"session"/utf8>>]), + _pipe@10 = priv@errors:map_error( + _pipe@9, + <<"Could not read \"session\" from aoc.toml"/utf8>> + ), + _pipe@11 = priv@errors:print_error(_pipe@10), + priv@errors:assert_ok(_pipe@11) + end, + Showtime = case Aoc_toml_version of + {ok, 2} -> + _pipe@12 = priv@toml:get_bool(Aoc_toml, [<<"showtime"/utf8>>]), + _pipe@13 = priv@errors:map_error( + _pipe@12, + <<"Could not read \"showtime\" from aoc.toml"/utf8>> + ), + _pipe@14 = priv@errors:print_error(_pipe@13), + priv@errors:assert_ok(_pipe@14); + + _ -> + _pipe@15 = priv@toml:get_string(Aoc_toml, [<<"showtime"/utf8>>]), + _pipe@16 = gleam@result:map( + _pipe@15, + fun(Bool_string) -> case Bool_string of + <<"True"/utf8>> -> + true; + + _ -> + false + end end + ), + _pipe@17 = priv@errors:map_error( + _pipe@16, + <<"Could not read \"showtime\" from aoc.toml"/utf8>> + ), + _pipe@18 = priv@errors:print_error(_pipe@17), + priv@errors:assert_ok(_pipe@18) + end, + Test_folder = adglent:get_test_folder(Day@1), + Test_file = <<<<<<Test_folder/binary, "/day"/utf8>>/binary, Day@1/binary>>/binary, + "_test.gleam"/utf8>>, + _pipe@19 = simplifile:create_directory_all(Test_folder), + _pipe@20 = priv@errors:map_error( + _pipe@19, + <<<<"Could not create folder \""/utf8, Test_folder/binary>>/binary, + "\""/utf8>> + ), + _pipe@21 = priv@errors:print_error(_pipe@20), + priv@errors:assert_ok(_pipe@21), + Testfile_template = case Showtime of + true -> + <<" +import gleam/list +import showtime/tests/should +import adglent.{type Example, Example} +import day{{ day }}/solve + +type Problem1AnswerType = + String + +type Problem2AnswerType = + String + +/// Add examples for part 1 here: +/// ```gleam +///const part1_examples: List(Example(Problem1AnswerType)) = [Example(\"some input\", \"\")] +/// ``` +const part1_examples: List(Example(Problem1AnswerType)) = [] + +/// Add examples for part 2 here: +/// ```gleam +///const part2_examples: List(Example(Problem2AnswerType)) = [Example(\"some input\", \"\")] +/// ``` +const part2_examples: List(Example(Problem2AnswerType)) = [] + +pub fn part1_test() { + part1_examples + |> should.not_equal([]) + use example <- list.map(part1_examples) + solve.part1(example.input) + |> should.equal(example.answer) +} + +pub fn part2_test() { + part2_examples + |> should.not_equal([]) + use example <- list.map(part2_examples) + solve.part2(example.input) + |> should.equal(example.answer) +} + +"/utf8>>; + + false -> + <<" +import gleam/list +import gleeunit/should +import adglent.{type Example, Example} +import day{{ day }}/solve + +type Problem1AnswerType = + String + +type Problem2AnswerType = + String + +/// Add examples for part 1 here: +/// ```gleam +///const part1_examples: List(Example(Problem1AnswerType)) = [Example(\"some input\", \"\")] +/// ``` +const part1_examples: List(Example(Problem1AnswerType)) = [] + +/// Add examples for part 2 here: +/// ```gleam +///const part2_examples: List(Example(Problem2AnswerType)) = [Example(\"some input\", \"\")] +/// ``` +const part2_examples: List(Example(Problem2AnswerType)) = [] + +pub fn part1_test() { + part1_examples + |> should.not_equal([]) + use example <- list.map(part1_examples) + solve.part1(example.input) + |> should.equal(example.answer) +} + +pub fn part2_test() { + part2_examples + |> should.not_equal([]) + use example <- list.map(part2_examples) + solve.part2(example.input) + |> should.equal(example.answer) +} + +"/utf8>> + end, + _pipe@22 = priv@template:render( + Testfile_template, + [{<<"day"/utf8>>, Day@1}] + ), + _pipe@23 = create_file_if_not_present(_pipe@22, Test_file), + _pipe@24 = priv@errors:print_result(_pipe@23), + priv@errors:assert_ok(_pipe@24), + Solutions_folder = <<"src/day"/utf8, Day@1/binary>>, + Solution_file = <<Solutions_folder/binary, "/solve.gleam"/utf8>>, + _pipe@25 = simplifile:create_directory_all(Solutions_folder), + _pipe@26 = priv@errors:map_error( + _pipe@25, + <<<<"Could not create folder \""/utf8, Solutions_folder/binary>>/binary, + "\""/utf8>> + ), + _pipe@27 = priv@errors:print_error(_pipe@26), + priv@errors:assert_ok(_pipe@27), + _pipe@28 = priv@template:render( + <<" +import adglent.{First, Second} +import gleam/io + +pub fn part1(input: String) { + todo as \"Implement solution to part 1\" +} + +pub fn part2(input: String) { + todo as \"Implement solution to part 2\" +} + +pub fn main() { + let assert Ok(part) = adglent.get_part() + let assert Ok(input) = adglent.get_input(\"{{ day }}\") + case part { + First -> + part1(input) + |> adglent.inspect + |> io.println + Second -> + part2(input) + |> adglent.inspect + |> io.println + } +} +"/utf8>>, + [{<<"day"/utf8>>, Day@1}] + ), + _pipe@29 = create_file_if_not_present(_pipe@28, Solution_file), + _pipe@30 = priv@errors:print_result(_pipe@29), + priv@errors:assert_ok(_pipe@30), + _pipe@31 = create_file_if_not_present( + <<"input.txt"/utf8>>, + <<Solutions_folder/binary, "/.gitignore"/utf8>> + ), + _pipe@32 = priv@errors:print_result(_pipe@31), + priv@errors:assert_ok(_pipe@32), + Input = begin + _pipe@33 = priv@aoc_client:get_input(Year, Day@1, Session), + _pipe@34 = priv@errors:map_error( + _pipe@33, + <<"Error when fetching input"/utf8>> + ), + _pipe@35 = priv@errors:print_error(_pipe@34), + priv@errors:assert_ok(_pipe@35) + end, + _pipe@36 = Input, + _pipe@37 = gleam@string:trim(_pipe@36), + _pipe@38 = create_file_if_not_present( + _pipe@37, + <<Solutions_folder/binary, "/input.txt"/utf8>> + ), + _pipe@39 = priv@errors:print_result(_pipe@38), + priv@errors:assert_ok(_pipe@39). diff --git a/aoc2023/build/packages/adglent/src/adglent@init.erl b/aoc2023/build/packages/adglent/src/adglent@init.erl new file mode 100644 index 0000000..fb28101 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/adglent@init.erl @@ -0,0 +1,142 @@ +-module(adglent@init). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([main/0]). + +-spec main() -> {ok, binary()} | {error, binary()}. +main() -> + Year = priv@prompt:value(<<"Year"/utf8>>, <<"2023"/utf8>>, false), + Session = priv@prompt:value(<<"Session Cookie"/utf8>>, <<""/utf8>>, false), + Use_showtime = priv@prompt:confirm(<<"Use showtime"/utf8>>, false), + Aoc_toml_file = <<"aoc.toml"/utf8>>, + Overwrite = case simplifile:create_file(Aoc_toml_file) of + {ok, _} -> + true; + + {error, eexist} -> + priv@prompt:confirm(<<"aoc.toml exits - overwrite"/utf8>>, false); + + _ -> + erlang:error(#{gleam_error => panic, + message => <<"Could not create aoc.toml"/utf8>>, + module => <<"adglent/init"/utf8>>, + function => <<"main"/utf8>>, + line => 29}) + end, + _pipe@3 = case Overwrite of + true -> + _pipe@1 = priv@template:render( + <<" +version = {{ version }} +year = \"{{ year }}\" +session = \"{{ session }}\" +showtime = {{ showtime }} +"/utf8>>, + [{<<"version"/utf8>>, <<"2"/utf8>>}, + {<<"year"/utf8>>, Year}, + {<<"session"/utf8>>, Session}, + {<<"showtime"/utf8>>, + begin + _pipe = gleam@bool:to_string(Use_showtime), + gleam@string:lowercase(_pipe) + end}] + ), + _pipe@2 = simplifile:write(Aoc_toml_file, _pipe@1), + priv@errors:map_messages( + _pipe@2, + <<"aoc.toml - written"/utf8>>, + <<"Error when writing aoc.toml"/utf8>> + ); + + false -> + {ok, <<"aoc.toml - skipped"/utf8>>} + end, + priv@errors:print_result(_pipe@3), + Gleam_toml = begin + _pipe@4 = simplifile:read(<<"gleam.toml"/utf8>>), + _pipe@5 = priv@errors:map_error( + _pipe@4, + <<"Could not read gleam.toml"/utf8>> + ), + _pipe@6 = priv@errors:print_error(_pipe@5), + priv@errors:assert_ok(_pipe@6) + end, + Name = begin + _pipe@7 = priv@toml:get_string(Gleam_toml, [<<"name"/utf8>>]), + _pipe@8 = priv@errors:map_error( + _pipe@7, + <<"Could not read \"name\" from gleam.toml"/utf8>> + ), + _pipe@9 = priv@errors:print_error(_pipe@8), + priv@errors:assert_ok(_pipe@9) + end, + Test_main_file = <<<<"test/"/utf8, Name/binary>>/binary, + "_test.gleam"/utf8>>, + _pipe@12 = case Use_showtime of + true -> + _pipe@10 = priv@template:render( + <<" +import showtime + +pub fn main() { + showtime.main() +} +"/utf8>>, + [] + ), + _pipe@11 = simplifile:write(Test_main_file, _pipe@10), + priv@errors:map_messages( + _pipe@11, + <<"Wrote "/utf8, Test_main_file/binary>>, + <<"Could not write to "/utf8, Test_main_file/binary>> + ); + + false -> + {ok, <<"Using existing (gleeunit) "/utf8, Test_main_file/binary>>} + end, + _pipe@13 = priv@errors:print_result(_pipe@12), + priv@errors:assert_ok(_pipe@13), + _pipe@17 = case simplifile:is_file(<<".gitignore"/utf8>>) of + true -> + gleam@result:'try'( + begin + _pipe@14 = simplifile:read(<<".gitignore"/utf8>>), + gleam@result:map_error( + _pipe@14, + fun(Err) -> + <<"Could not read .gitignore: "/utf8, + (gleam@string:inspect(Err))/binary>> + end + ) + end, + fun(Gitignore) -> + Aoc_toml_ignored = begin + _pipe@15 = gleam@string:split(Gitignore, <<"\n"/utf8>>), + gleam@list:find( + _pipe@15, + fun(Line) -> Line =:= <<"aoc.toml"/utf8>> end + ) + end, + case Aoc_toml_ignored of + {error, _} -> + _pipe@16 = simplifile:append( + <<".gitignore"/utf8>>, + <<"\naoc.toml"/utf8>> + ), + priv@errors:map_messages( + _pipe@16, + <<".gitignore written"/utf8>>, + <<"Error when writing .gitignore"/utf8>> + ); + + {ok, _} -> + {ok, + <<".gitignore - skipped (already configured)"/utf8>>} + end + end + ); + + false -> + {error, <<"Could not find .gitignore"/utf8>>} + end, + priv@errors:print_result(_pipe@17). diff --git a/aoc2023/build/packages/adglent/src/adglent_ffi.erl b/aoc2023/build/packages/adglent/src/adglent_ffi.erl new file mode 100644 index 0000000..a6a92e6 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/adglent_ffi.erl @@ -0,0 +1,12 @@ +-module(adglent_ffi). + +-export([get_line/1]). + +-spec get_line(io:prompt()) -> {ok, unicode:unicode_binary()} | {error, eof | no_data}. +get_line(Prompt) -> + case io:get_line(Prompt) of + eof -> {error, eof}; + {error, _} -> {error, no_data}; + Data when is_binary(Data) -> {ok, Data}; + Data when is_list(Data) -> {ok, unicode:characters_to_binary(Data)} + end. diff --git a/aoc2023/build/packages/adglent/src/priv/aoc_client.gleam b/aoc2023/build/packages/adglent/src/priv/aoc_client.gleam new file mode 100644 index 0000000..e18bafa --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv/aoc_client.gleam @@ -0,0 +1,37 @@ +import gleam/result.{try} +import gleam/httpc +import gleam/http/request +import gleam/int +import gleam/string + +pub fn get_input( + year: String, + day: String, + session: String, +) -> Result(String, String) { + let url = "https://adventofcode.com/" <> year <> "/day/" <> day <> "/input" + use request <- try( + request.to(url) + |> result.map_error(fn(error) { + "Could not create request for \"" <> url <> "\": " <> string.inspect( + error, + ) + }), + ) + + // Send the HTTP request to the server + use response <- try( + request + |> request.prepend_header("Accept", "application/json") + |> request.prepend_header("Cookie", "session=" <> session <> ";") + |> httpc.send + |> result.map_error(fn(error) { + "Error when requesting \"" <> url <> "\": " <> string.inspect(error) + }), + ) + + case response.status { + status if status >= 200 && status < 300 -> Ok(response.body) + status -> Error(int.to_string(status) <> " - " <> response.body) + } +} diff --git a/aoc2023/build/packages/adglent/src/priv/errors.gleam b/aoc2023/build/packages/adglent/src/priv/errors.gleam new file mode 100644 index 0000000..14c35ca --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv/errors.gleam @@ -0,0 +1,54 @@ +import gleam/result +import gleam/string +import gleam/io + +pub fn map_messages( + result: Result(a, b), + success_message: String, + error_message: String, +) -> Result(String, String) { + result + |> result.map_error(fn(error) { + "Error - " <> error_message <> ": " <> string.inspect(error) + }) + |> result.replace(success_message) +} + +pub fn map_error( + result: Result(a, b), + error_message: String, +) -> Result(a, String) { + result + |> result.map_error(fn(error) { + error_message <> ": " <> string.inspect(error) + }) +} + +pub fn print_result(result: Result(String, String)) { + result + |> result.unwrap_both + |> io.println + result +} + +pub fn print_error(result: Result(a, String)) { + result + |> result.map_error(fn(err) { + io.println(err) + err + }) +} + +pub fn assert_ok(result: Result(a, String)) { + let assert Ok(value) = + result + |> result.map_error(fn(err) { + halt(1) + err + }) + value +} + +@target(erlang) +@external(erlang, "erlang", "halt") +fn halt(a: Int) -> Nil diff --git a/aoc2023/build/packages/adglent/src/priv/prompt.gleam b/aoc2023/build/packages/adglent/src/priv/prompt.gleam new file mode 100644 index 0000000..6cee35a --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv/prompt.gleam @@ -0,0 +1,38 @@ +import gleam/result +import gleam/string + +pub fn confirm(message: String, auto_accept: Bool) -> Bool { + auto_accept || case + get_line(message <> "? (Y/N): ") + |> result.unwrap("n") + |> string.trim() + { + "Y" | "y" -> True + _ -> False + } +} + +pub fn value(message: String, default: String, auto_accept: Bool) -> String { + case get_value_of_default(message, default, auto_accept) { + "" -> default + value -> value + } +} + +fn get_value_of_default(message: String, default: String, auto_accept: Bool) { + case auto_accept { + True -> default + False -> + get_line(message <> "? (" <> default <> "): ") + |> result.unwrap("") + |> string.trim() + } +} + +pub type GetLineError { + Eof + NoData +} + +@external(erlang, "adglent_ffi", "get_line") +pub fn get_line(prompt prompt: String) -> Result(String, GetLineError) diff --git a/aoc2023/build/packages/adglent/src/priv/template.gleam b/aoc2023/build/packages/adglent/src/priv/template.gleam new file mode 100644 index 0000000..e946888 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv/template.gleam @@ -0,0 +1,18 @@ +import gleam/list +import gleam/string + +pub fn render( + template: String, + substitutions: List(#(String, String)), +) -> String { + substitutions + |> list.fold( + template, + fn(template, substitution) { + let #(name, value) = substitution + template + |> string.replace("{{ " <> name <> " }}", value) + }, + ) + |> string.trim <> "\n" +} diff --git a/aoc2023/build/packages/adglent/src/priv/templates/solution.gleam b/aoc2023/build/packages/adglent/src/priv/templates/solution.gleam new file mode 100644 index 0000000..96085c3 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv/templates/solution.gleam @@ -0,0 +1,27 @@ +pub const template = " +import adglent.{First, Second} +import gleam/io + +pub fn part1(input: String) { + todo as \"Implement solution to part 1\" +} + +pub fn part2(input: String) { + todo as \"Implement solution to part 2\" +} + +pub fn main() { + let assert Ok(part) = adglent.get_part() + let assert Ok(input) = adglent.get_input(\"{{ day }}\") + case part { + First -> + part1(input) + |> adglent.inspect + |> io.println + Second -> + part2(input) + |> adglent.inspect + |> io.println + } +} +" diff --git a/aoc2023/build/packages/adglent/src/priv/templates/test_main.gleam b/aoc2023/build/packages/adglent/src/priv/templates/test_main.gleam new file mode 100644 index 0000000..27548d3 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv/templates/test_main.gleam @@ -0,0 +1,7 @@ +pub const template = " +import showtime + +pub fn main() { + showtime.main() +} +" diff --git a/aoc2023/build/packages/adglent/src/priv/templates/testfile_gleeunit.gleam b/aoc2023/build/packages/adglent/src/priv/templates/testfile_gleeunit.gleam new file mode 100644 index 0000000..a1d56f6 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv/templates/testfile_gleeunit.gleam @@ -0,0 +1,41 @@ +pub const template = " +import gleam/list +import gleeunit/should +import adglent.{type Example, Example} +import day{{ day }}/solve + +type Problem1AnswerType = + String + +type Problem2AnswerType = + String + +/// Add examples for part 1 here: +/// ```gleam +///const part1_examples: List(Example(Problem1AnswerType)) = [Example(\"some input\", \"\")] +/// ``` +const part1_examples: List(Example(Problem1AnswerType)) = [] + +/// Add examples for part 2 here: +/// ```gleam +///const part2_examples: List(Example(Problem2AnswerType)) = [Example(\"some input\", \"\")] +/// ``` +const part2_examples: List(Example(Problem2AnswerType)) = [] + +pub fn part1_test() { + part1_examples + |> should.not_equal([]) + use example <- list.map(part1_examples) + solve.part1(example.input) + |> should.equal(example.answer) +} + +pub fn part2_test() { + part2_examples + |> should.not_equal([]) + use example <- list.map(part2_examples) + solve.part2(example.input) + |> should.equal(example.answer) +} + +" diff --git a/aoc2023/build/packages/adglent/src/priv/templates/testfile_showtime.gleam b/aoc2023/build/packages/adglent/src/priv/templates/testfile_showtime.gleam new file mode 100644 index 0000000..699feb2 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv/templates/testfile_showtime.gleam @@ -0,0 +1,41 @@ +pub const template = " +import gleam/list +import showtime/tests/should +import adglent.{type Example, Example} +import day{{ day }}/solve + +type Problem1AnswerType = + String + +type Problem2AnswerType = + String + +/// Add examples for part 1 here: +/// ```gleam +///const part1_examples: List(Example(Problem1AnswerType)) = [Example(\"some input\", \"\")] +/// ``` +const part1_examples: List(Example(Problem1AnswerType)) = [] + +/// Add examples for part 2 here: +/// ```gleam +///const part2_examples: List(Example(Problem2AnswerType)) = [Example(\"some input\", \"\")] +/// ``` +const part2_examples: List(Example(Problem2AnswerType)) = [] + +pub fn part1_test() { + part1_examples + |> should.not_equal([]) + use example <- list.map(part1_examples) + solve.part1(example.input) + |> should.equal(example.answer) +} + +pub fn part2_test() { + part2_examples + |> should.not_equal([]) + use example <- list.map(part2_examples) + solve.part2(example.input) + |> should.equal(example.answer) +} + +" diff --git a/aoc2023/build/packages/adglent/src/priv/toml.gleam b/aoc2023/build/packages/adglent/src/priv/toml.gleam new file mode 100644 index 0000000..7042833 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv/toml.gleam @@ -0,0 +1,52 @@ +import tom +import gleam/result + +pub type TomError { + TomParseError(error: tom.ParseError) + TomGetError(error: tom.GetError) +} + +pub fn get_string( + toml_content: String, + key_path: List(String), +) -> Result(String, TomError) { + use toml <- result.try( + tom.parse(toml_content <> "\n") + |> result.map_error(TomParseError), + ) + use value <- result.try( + tom.get_string(toml, key_path) + |> result.map_error(TomGetError), + ) + Ok(value) +} + +pub fn get_bool( + toml_content: String, + key_path: List(String), +) -> Result(Bool, TomError) { + use toml <- result.try( + tom.parse(toml_content <> "\n") + |> result.map_error(TomParseError), + ) + use value <- result.try( + tom.get_bool(toml, key_path) + |> result.map_error(TomGetError), + ) + Ok(value) +} + +pub fn get_int( + toml_content: String, + key_path: List(String), +) -> Result(Int, TomError) { + use toml <- result.try( + tom.parse(toml_content <> "\n") + |> result.map_error(TomParseError), + ) + use value <- result.try( + tom.get_int(toml, key_path) + |> result.map_error(TomGetError), + ) + Ok(value) +} diff --git a/aoc2023/build/packages/adglent/src/priv@aoc_client.erl b/aoc2023/build/packages/adglent/src/priv@aoc_client.erl new file mode 100644 index 0000000..1acb9b5 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv@aoc_client.erl @@ -0,0 +1,61 @@ +-module(priv@aoc_client). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([get_input/3]). + +-spec get_input(binary(), binary(), binary()) -> {ok, binary()} | + {error, binary()}. +get_input(Year, Day, Session) -> + Url = <<<<<<<<"https://adventofcode.com/"/utf8, Year/binary>>/binary, + "/day/"/utf8>>/binary, + Day/binary>>/binary, + "/input"/utf8>>, + gleam@result:'try'( + begin + _pipe = gleam@http@request:to(Url), + gleam@result:map_error( + _pipe, + fun(Error) -> + <<<<<<"Could not create request for \""/utf8, Url/binary>>/binary, + "\": "/utf8>>/binary, + (gleam@string:inspect(Error))/binary>> + end + ) + end, + fun(Request) -> + gleam@result:'try'( + begin + _pipe@1 = Request, + _pipe@2 = gleam@http@request:prepend_header( + _pipe@1, + <<"Accept"/utf8>>, + <<"application/json"/utf8>> + ), + _pipe@3 = gleam@http@request:prepend_header( + _pipe@2, + <<"Cookie"/utf8>>, + <<<<"session="/utf8, Session/binary>>/binary, ";"/utf8>> + ), + _pipe@4 = gleam@httpc:send(_pipe@3), + gleam@result:map_error( + _pipe@4, + fun(Error@1) -> + <<<<<<"Error when requesting \""/utf8, Url/binary>>/binary, + "\": "/utf8>>/binary, + (gleam@string:inspect(Error@1))/binary>> + end + ) + end, + fun(Response) -> case erlang:element(2, Response) of + Status when (Status >= 200) andalso (Status < 300) -> + {ok, erlang:element(4, Response)}; + + Status@1 -> + {error, + <<<<(gleam@int:to_string(Status@1))/binary, + " - "/utf8>>/binary, + (erlang:element(4, Response))/binary>>} + end end + ) + end + ). diff --git a/aoc2023/build/packages/adglent/src/priv@errors.erl b/aoc2023/build/packages/adglent/src/priv@errors.erl new file mode 100644 index 0000000..978c675 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv@errors.erl @@ -0,0 +1,74 @@ +-module(priv@errors). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([map_messages/3, map_error/2, print_result/1, print_error/1, assert_ok/1]). + +-spec map_messages({ok, any()} | {error, any()}, binary(), binary()) -> {ok, + binary()} | + {error, binary()}. +map_messages(Result, Success_message, Error_message) -> + _pipe = Result, + _pipe@1 = gleam@result:map_error( + _pipe, + fun(Error) -> + <<<<<<"Error - "/utf8, Error_message/binary>>/binary, ": "/utf8>>/binary, + (gleam@string:inspect(Error))/binary>> + end + ), + gleam@result:replace(_pipe@1, Success_message). + +-spec map_error({ok, NIB} | {error, any()}, binary()) -> {ok, NIB} | + {error, binary()}. +map_error(Result, Error_message) -> + _pipe = Result, + gleam@result:map_error( + _pipe, + fun(Error) -> + <<<<Error_message/binary, ": "/utf8>>/binary, + (gleam@string:inspect(Error))/binary>> + end + ). + +-spec print_result({ok, binary()} | {error, binary()}) -> {ok, binary()} | + {error, binary()}. +print_result(Result) -> + _pipe = Result, + _pipe@1 = gleam@result:unwrap_both(_pipe), + gleam@io:println(_pipe@1), + Result. + +-spec print_error({ok, NIK} | {error, binary()}) -> {ok, NIK} | + {error, binary()}. +print_error(Result) -> + _pipe = Result, + gleam@result:map_error( + _pipe, + fun(Err) -> + gleam@io:println(Err), + Err + end + ). + +-spec assert_ok({ok, NIO} | {error, binary()}) -> NIO. +assert_ok(Result) -> + _assert_subject = begin + _pipe = Result, + gleam@result:map_error( + _pipe, + fun(Err) -> + erlang:halt(1), + Err + end + ) + end, + {ok, Value} = case _assert_subject of + {ok, _} -> _assert_subject; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail, + module => <<"priv/errors"/utf8>>, + function => <<"assert_ok"/utf8>>, + line => 43}) + end, + Value. diff --git a/aoc2023/build/packages/adglent/src/priv@prompt.erl b/aoc2023/build/packages/adglent/src/priv@prompt.erl new file mode 100644 index 0000000..0277f14 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv@prompt.erl @@ -0,0 +1,53 @@ +-module(priv@prompt). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([get_line/1, confirm/2, value/3]). +-export_type([get_line_error/0]). + +-type get_line_error() :: eof | no_data. + +-spec get_line(binary()) -> {ok, binary()} | {error, get_line_error()}. +get_line(Prompt) -> + adglent_ffi:get_line(Prompt). + +-spec confirm(binary(), boolean()) -> boolean(). +confirm(Message, Auto_accept) -> + Auto_accept orelse case begin + _pipe = adglent_ffi:get_line(<<Message/binary, "? (Y/N): "/utf8>>), + _pipe@1 = gleam@result:unwrap(_pipe, <<"n"/utf8>>), + gleam@string:trim(_pipe@1) + end of + <<"Y"/utf8>> -> + true; + + <<"y"/utf8>> -> + true; + + _ -> + false + end. + +-spec get_value_of_default(binary(), binary(), boolean()) -> binary(). +get_value_of_default(Message, Default, Auto_accept) -> + case Auto_accept of + true -> + Default; + + false -> + _pipe = adglent_ffi:get_line( + <<<<<<Message/binary, "? ("/utf8>>/binary, Default/binary>>/binary, + "): "/utf8>> + ), + _pipe@1 = gleam@result:unwrap(_pipe, <<""/utf8>>), + gleam@string:trim(_pipe@1) + end. + +-spec value(binary(), binary(), boolean()) -> binary(). +value(Message, Default, Auto_accept) -> + case get_value_of_default(Message, Default, Auto_accept) of + <<""/utf8>> -> + Default; + + Value -> + Value + end. diff --git a/aoc2023/build/packages/adglent/src/priv@template.erl b/aoc2023/build/packages/adglent/src/priv@template.erl new file mode 100644 index 0000000..6a5d0bf --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv@template.erl @@ -0,0 +1,25 @@ +-module(priv@template). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([render/2]). + +-spec render(binary(), list({binary(), binary()})) -> binary(). +render(Template, Substitutions) -> + <<(begin + _pipe = Substitutions, + _pipe@2 = gleam@list:fold( + _pipe, + Template, + fun(Template@1, Substitution) -> + {Name, Value} = Substitution, + _pipe@1 = Template@1, + gleam@string:replace( + _pipe@1, + <<<<"{{ "/utf8, Name/binary>>/binary, " }}"/utf8>>, + Value + ) + end + ), + gleam@string:trim(_pipe@2) + end)/binary, + "\n"/utf8>>. diff --git a/aoc2023/build/packages/adglent/src/priv@templates@solution.erl b/aoc2023/build/packages/adglent/src/priv@templates@solution.erl new file mode 100644 index 0000000..7e36387 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv@templates@solution.erl @@ -0,0 +1 @@ +-module(priv@templates@solution). diff --git a/aoc2023/build/packages/adglent/src/priv@templates@test_main.erl b/aoc2023/build/packages/adglent/src/priv@templates@test_main.erl new file mode 100644 index 0000000..ca6b127 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv@templates@test_main.erl @@ -0,0 +1 @@ +-module(priv@templates@test_main). diff --git a/aoc2023/build/packages/adglent/src/priv@templates@testfile_gleeunit.erl b/aoc2023/build/packages/adglent/src/priv@templates@testfile_gleeunit.erl new file mode 100644 index 0000000..2f5a41e --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv@templates@testfile_gleeunit.erl @@ -0,0 +1 @@ +-module(priv@templates@testfile_gleeunit). diff --git a/aoc2023/build/packages/adglent/src/priv@templates@testfile_showtime.erl b/aoc2023/build/packages/adglent/src/priv@templates@testfile_showtime.erl new file mode 100644 index 0000000..bbbc8b2 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv@templates@testfile_showtime.erl @@ -0,0 +1 @@ +-module(priv@templates@testfile_showtime). diff --git a/aoc2023/build/packages/adglent/src/priv@toml.erl b/aoc2023/build/packages/adglent/src/priv@toml.erl new file mode 100644 index 0000000..6c41fbf --- /dev/null +++ b/aoc2023/build/packages/adglent/src/priv@toml.erl @@ -0,0 +1,83 @@ +-module(priv@toml). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([get_string/2, get_bool/2, get_int/2]). +-export_type([tom_error/0]). + +-type tom_error() :: {tom_parse_error, tom:parse_error()} | + {tom_get_error, tom:get_error()}. + +-spec get_string(binary(), list(binary())) -> {ok, binary()} | + {error, tom_error()}. +get_string(Toml_content, Key_path) -> + gleam@result:'try'( + begin + _pipe = tom:parse(<<Toml_content/binary, "\n"/utf8>>), + gleam@result:map_error( + _pipe, + fun(Field@0) -> {tom_parse_error, Field@0} end + ) + end, + fun(Toml) -> + gleam@result:'try'( + begin + _pipe@1 = tom:get_string(Toml, Key_path), + gleam@result:map_error( + _pipe@1, + fun(Field@0) -> {tom_get_error, Field@0} end + ) + end, + fun(Value) -> {ok, Value} end + ) + end + ). + +-spec get_bool(binary(), list(binary())) -> {ok, boolean()} | + {error, tom_error()}. +get_bool(Toml_content, Key_path) -> + gleam@result:'try'( + begin + _pipe = tom:parse(<<Toml_content/binary, "\n"/utf8>>), + gleam@result:map_error( + _pipe, + fun(Field@0) -> {tom_parse_error, Field@0} end + ) + end, + fun(Toml) -> + gleam@result:'try'( + begin + _pipe@1 = tom:get_bool(Toml, Key_path), + gleam@result:map_error( + _pipe@1, + fun(Field@0) -> {tom_get_error, Field@0} end + ) + end, + fun(Value) -> {ok, Value} end + ) + end + ). + +-spec get_int(binary(), list(binary())) -> {ok, integer()} | + {error, tom_error()}. +get_int(Toml_content, Key_path) -> + gleam@result:'try'( + begin + _pipe = tom:parse(<<Toml_content/binary, "\n"/utf8>>), + gleam@result:map_error( + _pipe, + fun(Field@0) -> {tom_parse_error, Field@0} end + ) + end, + fun(Toml) -> + gleam@result:'try'( + begin + _pipe@1 = tom:get_int(Toml, Key_path), + gleam@result:map_error( + _pipe@1, + fun(Field@0) -> {tom_get_error, Field@0} end + ) + end, + fun(Value) -> {ok, Value} end + ) + end + ). diff --git a/aoc2023/build/packages/adglent/src/showtime.erl b/aoc2023/build/packages/adglent/src/showtime.erl new file mode 100644 index 0000000..721bad4 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime.erl @@ -0,0 +1,155 @@ +-module(showtime). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([main/0]). + +-spec mk_runner( + fun((gleam@option:option(list(binary())), list(binary()), showtime@internal@common@cli:capture()) -> OBY), + glint:command_input() +) -> OBY. +mk_runner(Func, Command) -> + _assert_subject = begin + _pipe = erlang:element(3, Command), + _pipe@1 = glint@flag:get_strings(_pipe, <<"modules"/utf8>>), + gleam@result:map(_pipe@1, fun(Modules) -> case Modules of + [] -> + none; + + Modules@1 -> + {some, Modules@1} + end end) + end, + {ok, Module_list} = case _assert_subject of + {ok, _} -> _assert_subject; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail, + module => <<"showtime"/utf8>>, + function => <<"mk_runner"/utf8>>, + line => 91}) + end, + _assert_subject@1 = begin + _pipe@2 = erlang:element(3, Command), + glint@flag:get_strings(_pipe@2, <<"ignore"/utf8>>) + end, + {ok, Ignore_tags} = case _assert_subject@1 of + {ok, _} -> _assert_subject@1; + _assert_fail@1 -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail@1, + module => <<"showtime"/utf8>>, + function => <<"mk_runner"/utf8>>, + line => 100}) + end, + _assert_subject@2 = begin + _pipe@3 = erlang:element(3, Command), + _pipe@4 = glint@flag:get_string(_pipe@3, <<"capture"/utf8>>), + _pipe@5 = gleam@result:map( + _pipe@4, + fun(Arg) -> gleam@string:lowercase(Arg) end + ), + gleam@result:map(_pipe@5, fun(Arg@1) -> case Arg@1 of + <<"no"/utf8>> -> + no; + + <<"yes"/utf8>> -> + yes; + + <<"mixed"/utf8>> -> + mixed + end end) + end, + {ok, Capture_output} = case _assert_subject@2 of + {ok, _} -> _assert_subject@2; + _assert_fail@2 -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail@2, + module => <<"showtime"/utf8>>, + function => <<"mk_runner"/utf8>>, + line => 104}) + end, + Func(Module_list, Ignore_tags, Capture_output). + +-spec start_with_args( + list(binary()), + fun((gleam@option:option(list(binary())), list(binary()), showtime@internal@common@cli:capture()) -> any()) +) -> nil. +start_with_args(Args, Func) -> + Modules_flag = begin + _pipe = glint@flag:string_list(), + _pipe@1 = glint@flag:default(_pipe, []), + glint@flag:description( + _pipe@1, + <<"Run only tests in the modules in this list"/utf8>> + ) + end, + Ignore_flag = begin + _pipe@2 = glint@flag:string_list(), + _pipe@3 = glint@flag:default(_pipe@2, []), + glint@flag:description( + _pipe@3, + <<"Ignore tests that are have tags matching a tag in this list"/utf8>> + ) + end, + Capture_flag = begin + _pipe@4 = glint@flag:string(), + _pipe@5 = glint@flag:default(_pipe@4, <<"no"/utf8>>), + _pipe@6 = glint@flag:constraint( + _pipe@5, + glint@flag@constraint:one_of( + [<<"yes"/utf8>>, <<"no"/utf8>>, <<"mixed"/utf8>>] + ) + ), + glint@flag:description( + _pipe@6, + <<"Capture output: no (default) - output when tests are run, yes - output is captured and shown in report, mixed - output when run and in report"/utf8>> + ) + end, + _pipe@7 = glint:new(), + _pipe@12 = glint:add( + _pipe@7, + [], + begin + _pipe@8 = glint:command( + fun(_capture) -> mk_runner(Func, _capture) end + ), + _pipe@9 = glint:flag(_pipe@8, <<"modules"/utf8>>, Modules_flag), + _pipe@10 = glint:flag(_pipe@9, <<"ignore"/utf8>>, Ignore_flag), + _pipe@11 = glint:flag(_pipe@10, <<"capture"/utf8>>, Capture_flag), + glint:description(_pipe@11, <<"Runs test"/utf8>>) + end + ), + _pipe@13 = glint:with_pretty_help(_pipe@12, glint:default_pretty_help()), + glint:run(_pipe@13, Args). + +-spec main() -> nil. +main() -> + start_with_args( + gleam@erlang:start_arguments(), + fun(Module_list, Ignore_tags, Capture) -> + Test_event_handler = showtime@internal@erlang@event_handler:start(), + Test_module_handler = showtime@internal@erlang@module_handler:start( + Test_event_handler, + fun showtime@internal@erlang@discover:collect_test_functions/1, + fun showtime@internal@erlang@runner:run_test_suite/4, + Ignore_tags, + Capture + ), + Test_event_handler(start_test_run), + Modules = showtime@internal@erlang@discover:collect_modules( + Test_module_handler, + Module_list + ), + Test_event_handler( + {end_test_run, + begin + _pipe = Modules, + gleam@list:length(_pipe) + end} + ), + nil + end + ). diff --git a/aoc2023/build/packages/adglent/src/showtime.gleam b/aoc2023/build/packages/adglent/src/showtime.gleam new file mode 100644 index 0000000..f0401c9 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime.gleam @@ -0,0 +1,116 @@ +import glint.{type CommandInput, flag} +import glint/flag +import glint/flag/constraint.{one_of} +import gleam/result +import gleam/string +import showtime/internal/common/cli.{Mixed, No, Yes} +@target(erlang) +import gleam/list +@target(erlang) +import gleam/option.{None, Some} +@target(erlang) +import gleam/erlang.{start_arguments} +@target(erlang) +import showtime/internal/common/test_suite.{EndTestRun, StartTestRun} +@target(erlang) +import showtime/internal/erlang/event_handler +@target(erlang) +import showtime/internal/erlang/module_handler +@target(erlang) +import showtime/internal/erlang/runner +@target(erlang) +import showtime/internal/erlang/discover.{ + collect_modules, collect_test_functions, +} + +// @target(javascript) +// import gleam/io + +@target(erlang) +pub fn main() { + use module_list, ignore_tags, capture <- start_with_args(start_arguments()) + // Start event handler which will collect test-results and eventually + // print test report + let test_event_handler = event_handler.start() + // Start module handler which receives msg about modules to test and + // runs the test-suite for the module + let test_module_handler = + module_handler.start( + test_event_handler, + collect_test_functions, + runner.run_test_suite, + ignore_tags, + capture, + ) + + test_event_handler(StartTestRun) + // Collect modules and notify the module handler to start the test-suites + let modules = collect_modules(test_module_handler, module_list) + test_event_handler(EndTestRun( + modules + |> list.length(), + )) + Nil +} + +fn start_with_args(args, func) { + let modules_flag = + flag.string_list() + |> flag.default([]) + |> flag.description("Run only tests in the modules in this list") + + let ignore_flag = + flag.string_list() + |> flag.default([]) + |> flag.description( + "Ignore tests that are have tags matching a tag in this list", + ) + + let capture_flag = + flag.string() + |> flag.default("no") + |> flag.constraint(one_of(["yes", "no", "mixed"])) + |> flag.description( + "Capture output: no (default) - output when tests are run, yes - output is captured and shown in report, mixed - output when run and in report", + ) + + glint.new() + |> glint.add( + at: [], + do: glint.command(mk_runner(func, _)) + |> glint.flag("modules", modules_flag) + |> glint.flag("ignore", ignore_flag) + |> glint.flag("capture", capture_flag) + |> glint.description("Runs test"), + ) + |> glint.with_pretty_help(glint.default_pretty_help()) + |> glint.run(args) +} + +fn mk_runner(func, command: CommandInput) { + let assert Ok(module_list) = + command.flags + |> flag.get_strings("modules") + |> result.map(fn(modules) { + case modules { + [] -> None + modules -> Some(modules) + } + }) + let assert Ok(ignore_tags) = + command.flags + |> flag.get_strings("ignore") + + let assert Ok(capture_output) = + command.flags + |> flag.get_string("capture") + |> result.map(fn(arg) { string.lowercase(arg) }) + |> result.map(fn(arg) { + case arg { + "no" -> No + "yes" -> Yes + "mixed" -> Mixed + } + }) + func(module_list, ignore_tags, capture_output) +} 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) +} diff --git a/aoc2023/build/packages/adglent/src/showtime@internal@common@cli.erl b/aoc2023/build/packages/adglent/src/showtime@internal@common@cli.erl new file mode 100644 index 0000000..f2d2396 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@internal@common@cli.erl @@ -0,0 +1,8 @@ +-module(showtime@internal@common@cli). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export_type([capture/0]). + +-type capture() :: yes | no | mixed. + + diff --git a/aoc2023/build/packages/adglent/src/showtime@internal@common@common_event_handler.erl b/aoc2023/build/packages/adglent/src/showtime@internal@common@common_event_handler.erl new file mode 100644 index 0000000..b0a6d7a --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@internal@common@common_event_handler.erl @@ -0,0 +1,131 @@ +-module(showtime@internal@common@common_event_handler). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([handle_event/3]). +-export_type([test_state/0, handler_state/0]). + +-type test_state() :: not_started | running | {finished, integer()}. + +-type handler_state() :: {handler_state, + test_state(), + integer(), + gleam@map:map_(binary(), gleam@map:map_(binary(), showtime@internal@common@test_suite:test_run()))}. + +-spec handle_event( + showtime@internal@common@test_suite:test_event(), + fun(() -> integer()), + handler_state() +) -> handler_state(). +handle_event(Msg, System_time, State) -> + Test_state = erlang:element(2, State), + Num_done = erlang:element(3, State), + Events = erlang:element(4, State), + {Updated_test_state, Updated_num_done, Updated_events} = case Msg of + start_test_run -> + {running, Num_done, Events}; + + {start_test_suite, Module} -> + Maybe_module_events = gleam@map:get( + Events, + erlang:element(2, Module) + ), + New_events = case Maybe_module_events of + {ok, _} -> + Events; + + {error, _} -> + _pipe = Events, + gleam@map:insert( + _pipe, + erlang:element(2, Module), + gleam@map:new() + ) + end, + {Test_state, Num_done, New_events}; + + {start_test, Module@1, Test} -> + Current_time = System_time(), + Maybe_module_events@1 = gleam@map:get( + Events, + erlang:element(2, Module@1) + ), + New_events@1 = case Maybe_module_events@1 of + {ok, Module_events} -> + Maybe_test_event = gleam@map:get( + Module_events, + erlang:element(2, Test) + ), + case Maybe_test_event of + {error, _} -> + _pipe@1 = Events, + gleam@map:insert( + _pipe@1, + erlang:element(2, Module@1), + begin + _pipe@2 = Module_events, + gleam@map:insert( + _pipe@2, + erlang:element(2, Test), + {ongoing_test_run, Test, Current_time} + ) + end + ); + + {ok, _} -> + Events + end; + + {error, _} -> + Events + end, + {Test_state, Num_done, New_events@1}; + + {end_test, Module@2, Test@1, Result} -> + Current_time@1 = System_time(), + Maybe_module_events@2 = gleam@map:get( + Events, + erlang:element(2, Module@2) + ), + New_events@2 = case Maybe_module_events@2 of + {ok, Module_events@1} -> + Maybe_test_run = begin + _pipe@3 = Module_events@1, + gleam@map:get(_pipe@3, erlang:element(2, Test@1)) + end, + Updated_module_events = case Maybe_test_run of + {ok, {ongoing_test_run, Test_function, Started_at}} -> + _pipe@4 = Module_events@1, + gleam@map:insert( + _pipe@4, + erlang:element(2, Test@1), + {completed_test_run, + Test_function, + Current_time@1 - Started_at, + Result} + ); + + {error, _} -> + Module_events@1 + end, + _pipe@5 = Events, + gleam@map:insert( + _pipe@5, + erlang:element(2, Module@2), + Updated_module_events + ); + + {error, _} -> + Events + end, + {Test_state, Num_done, New_events@2}; + + {end_test_suite, _} -> + {Test_state, Num_done + 1, Events}; + + {end_test_run, Num_modules} -> + {{finished, Num_modules}, Num_done, Events}; + + _ -> + {running, Num_done, Events} + end, + {handler_state, Updated_test_state, Updated_num_done, Updated_events}. diff --git a/aoc2023/build/packages/adglent/src/showtime@internal@common@test_result.erl b/aoc2023/build/packages/adglent/src/showtime@internal@common@test_result.erl new file mode 100644 index 0000000..b7b73be --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@internal@common@test_result.erl @@ -0,0 +1,54 @@ +-module(showtime@internal@common@test_result). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export_type([ignore_reason/0, test_return/0, exception/0, reason/0, reason_detail/0, gleam_error_detail/0, class/0, trace_list/0, trace/0, extra_info/0, arity_/0]). + +-type ignore_reason() :: ignore. + +-type test_return() :: {test_function_return, + gleam@dynamic:dynamic_(), + list(binary())} | + {ignored, ignore_reason()}. + +-type exception() :: {erlang_exception, + class(), + reason(), + trace_list(), + list(binary())}. + +-type reason() :: {assert_equal, list(reason_detail())} | + {assert_not_equal, list(reason_detail())} | + {assert_match, list(reason_detail())} | + {gleam_error, gleam_error_detail()} | + {gleam_assert, gleam@dynamic:dynamic_(), integer()} | + {generic_exception, gleam@dynamic:dynamic_()}. + +-type reason_detail() :: {module, binary()} | + {reason_line, integer()} | + {expression, binary()} | + {expected, gleam@dynamic:dynamic_()} | + {value, gleam@dynamic:dynamic_()} | + {pattern, binary()}. + +-type gleam_error_detail() :: {let_assert, + binary(), + binary(), + integer(), + binary(), + gleam@dynamic:dynamic_()}. + +-type class() :: erlang_error | exit | throw. + +-type trace_list() :: {trace_list, list(trace())}. + +-type trace() :: {trace, binary(), arity_(), list(extra_info())} | + {trace_module, binary(), binary(), arity_(), list(extra_info())}. + +-type extra_info() :: {error_info, + gleam@map:map_(gleam@dynamic:dynamic_(), gleam@dynamic:dynamic_())} | + {file, binary()} | + {line, integer()}. + +-type arity_() :: {num, integer()} | {arg_list, list(gleam@dynamic:dynamic_())}. + + diff --git a/aoc2023/build/packages/adglent/src/showtime@internal@common@test_suite.erl b/aoc2023/build/packages/adglent/src/showtime@internal@common@test_suite.erl new file mode 100644 index 0000000..6a56de8 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@internal@common@test_suite.erl @@ -0,0 +1,30 @@ +-module(showtime@internal@common@test_suite). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export_type([test_run/0, test_module/0, test_function/0, test_suite/0, test_event/0]). + +-type test_run() :: {ongoing_test_run, test_function(), integer()} | + {completed_test_run, + test_function(), + integer(), + {ok, showtime@internal@common@test_result:test_return()} | + {error, showtime@internal@common@test_result:exception()}}. + +-type test_module() :: {test_module, binary(), gleam@option:option(binary())}. + +-type test_function() :: {test_function, binary()}. + +-type test_suite() :: {test_suite, test_module(), list(test_function())}. + +-type test_event() :: start_test_run | + {start_test_suite, test_module()} | + {start_test, test_module(), test_function()} | + {end_test, + test_module(), + test_function(), + {ok, showtime@internal@common@test_result:test_return()} | + {error, showtime@internal@common@test_result:exception()}} | + {end_test_suite, test_module()} | + {end_test_run, integer()}. + + diff --git a/aoc2023/build/packages/adglent/src/showtime@internal@erlang@discover.erl b/aoc2023/build/packages/adglent/src/showtime@internal@erlang@discover.erl new file mode 100644 index 0000000..f0548aa --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@internal@erlang@discover.erl @@ -0,0 +1,230 @@ +-module(showtime@internal@erlang@discover). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([collect_modules/2, collect_test_functions/1]). + +-spec get_module_prefix(binary()) -> binary(). +get_module_prefix(Path) -> + Path_without_test = begin + _pipe = Path, + gleam@string:replace(_pipe, <<"./test"/utf8>>, <<""/utf8>>) + end, + Path_without_leading_slash = case gleam@string:starts_with( + Path_without_test, + <<"/"/utf8>> + ) of + true -> + gleam@string:drop_left(Path_without_test, 1); + + false -> + Path_without_test + end, + Module_prefix = begin + _pipe@1 = Path_without_leading_slash, + gleam@string:replace(_pipe@1, <<"/"/utf8>>, <<"@"/utf8>>) + end, + case gleam@string:length(Module_prefix) of + 0 -> + Module_prefix; + + _ -> + <<Module_prefix/binary, "@"/utf8>> + end. + +-spec collect_modules_in_folder( + binary(), + fun((showtime@internal@common@test_suite:test_module()) -> nil), + gleam@option:option(list(binary())) +) -> list(showtime@internal@common@test_suite:test_module()). +collect_modules_in_folder(Path, Test_module_handler, Only_modules) -> + Module_prefix = get_module_prefix(Path), + _assert_subject = simplifile:read_directory(Path), + {ok, Files} = case _assert_subject of + {ok, _} -> _assert_subject; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail, + module => <<"showtime/internal/erlang/discover"/utf8>>, + function => <<"collect_modules_in_folder"/utf8>>, + line => 40}) + end, + Test_modules_in_folder = begin + _pipe = Files, + _pipe@1 = gleam@list:filter( + _pipe, + fun(_capture) -> + gleam@string:ends_with(_capture, <<"_test.gleam"/utf8>>) + end + ), + gleam@list:filter_map( + _pipe@1, + fun(Test_module_file) -> + Module_name = <<Module_prefix/binary, + (begin + _pipe@2 = Test_module_file, + gleam@string:replace( + _pipe@2, + <<".gleam"/utf8>>, + <<""/utf8>> + ) + end)/binary>>, + case Only_modules of + {some, Only_modules_list} -> + Module_in_list = begin + _pipe@3 = Only_modules_list, + gleam@list:any( + _pipe@3, + fun(Only_module_name) -> + Only_module_name =:= begin + _pipe@4 = Module_name, + gleam@string:replace( + _pipe@4, + <<"@"/utf8>>, + <<"/"/utf8>> + ) + end + end + ) + end, + case Module_in_list of + true -> + Test_module = {test_module, + Module_name, + {some, Test_module_file}}, + Test_module_handler(Test_module), + {ok, Test_module}; + + false -> + {error, nil} + end; + + none -> + Test_module@1 = {test_module, + Module_name, + {some, Test_module_file}}, + Test_module_handler(Test_module@1), + {ok, Test_module@1} + end + end + ) + end, + Test_modules_in_subfolders = begin + _pipe@5 = Files, + _pipe@6 = gleam@list:map( + _pipe@5, + fun(Filename) -> + <<<<Path/binary, "/"/utf8>>/binary, Filename/binary>> + end + ), + _pipe@7 = gleam@list:filter( + _pipe@6, + fun(File) -> simplifile:is_directory(File) end + ), + gleam@list:fold( + _pipe@7, + [], + fun(Modules, Subfolder) -> _pipe@8 = Modules, + gleam@list:append( + _pipe@8, + collect_modules_in_folder( + Subfolder, + Test_module_handler, + Only_modules + ) + ) end + ) + end, + _pipe@9 = Test_modules_in_folder, + gleam@list:append(_pipe@9, Test_modules_in_subfolders). + +-spec collect_modules( + fun((showtime@internal@common@test_suite:test_module()) -> nil), + gleam@option:option(list(binary())) +) -> list(showtime@internal@common@test_suite:test_module()). +collect_modules(Test_module_handler, Only_modules) -> + collect_modules_in_folder( + <<"./test"/utf8>>, + Test_module_handler, + Only_modules + ). + +-spec collect_test_functions(showtime@internal@common@test_suite:test_module()) -> showtime@internal@common@test_suite:test_suite(). +collect_test_functions(Module) -> + Test_functions = begin + _pipe = erlang:apply( + erlang:binary_to_atom(erlang:element(2, Module)), + erlang:binary_to_atom(<<"module_info"/utf8>>), + [gleam@dynamic:from(erlang:binary_to_atom(<<"exports"/utf8>>))] + ), + gleam@dynamic:unsafe_coerce(_pipe) + end, + Test_functions_filtered = begin + _pipe@1 = Test_functions, + _pipe@3 = gleam@list:map( + _pipe@1, + fun(Entry) -> + {Name, Arity} = case Entry of + {_, _} -> Entry; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail, + module => <<"showtime/internal/erlang/discover"/utf8>>, + function => <<"collect_test_functions"/utf8>>, + line => 131}) + end, + {begin + _pipe@2 = Name, + erlang:atom_to_binary(_pipe@2) + end, + Arity} + end + ), + _pipe@4 = gleam@list:filter_map( + _pipe@3, + fun(Entry@1) -> + {Name@1, Arity@1} = case Entry@1 of + {_, _} -> Entry@1; + _assert_fail@1 -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail@1, + module => <<"showtime/internal/erlang/discover"/utf8>>, + function => <<"collect_test_functions"/utf8>>, + line => 139}) + end, + case gleam@string:ends_with(Name@1, <<"_test"/utf8>>) of + true -> + case Arity@1 of + 0 -> + {ok, Name@1}; + + _ -> + gleam@io:println( + <<<<<<<<"WARNING: function \""/utf8, + Name@1/binary>>/binary, + "\" has arity: "/utf8>>/binary, + (gleam@int:to_string(Arity@1))/binary>>/binary, + " - cannot be used as test (needs to be 0)"/utf8>> + ), + {error, <<"Wrong arity"/utf8>>} + end; + + false -> + {error, <<"Non matching name"/utf8>>} + end + end + ), + _pipe@5 = gleam@list:filter( + _pipe@4, + fun(_capture) -> + gleam@string:ends_with(_capture, <<"_test"/utf8>>) + end + ), + gleam@list:map( + _pipe@5, + fun(Function_name) -> {test_function, Function_name} end + ) + end, + {test_suite, Module, Test_functions_filtered}. diff --git a/aoc2023/build/packages/adglent/src/showtime@internal@erlang@event_handler.erl b/aoc2023/build/packages/adglent/src/showtime@internal@erlang@event_handler.erl new file mode 100644 index 0000000..d72ce2c --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@internal@erlang@event_handler.erl @@ -0,0 +1,76 @@ +-module(showtime@internal@erlang@event_handler). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([start/0]). +-export_type([event_handler_message/0]). + +-type event_handler_message() :: {event_handler_message, + showtime@internal@common@test_suite:test_event(), + gleam@erlang@process:subject(integer())}. + +-spec system_time() -> integer(). +system_time() -> + os:system_time(millisecond). + +-spec start() -> fun((showtime@internal@common@test_suite:test_event()) -> nil). +start() -> + _assert_subject = gleam@otp@actor:start( + {not_started, 0, gleam@map:new()}, + fun(Msg, State) -> + {event_handler_message, Test_event, Reply_to} = Msg, + {Test_state, Num_done, Events} = State, + Updated_state = showtime@internal@common@common_event_handler:handle_event( + Test_event, + fun system_time/0, + {handler_state, Test_state, Num_done, Events} + ), + case Updated_state of + {handler_state, {finished, Num_modules}, Num_done@1, Events@1} when Num_done@1 =:= Num_modules -> + {Test_report, Num_failed} = showtime@internal@reports@formatter:create_test_report( + Events@1 + ), + gleam@io:println(Test_report), + gleam@erlang@process:send(Reply_to, Num_failed), + {stop, normal}; + + {handler_state, Test_state@1, Num_done@2, Events@2} -> + {continue, {Test_state@1, Num_done@2, Events@2}, none} + end + end + ), + {ok, Subject} = case _assert_subject of + {ok, _} -> _assert_subject; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail, + module => <<"showtime/internal/erlang/event_handler"/utf8>>, + function => <<"start"/utf8>>, + line => 32}) + end, + Parent_subject = gleam@erlang@process:new_subject(), + Selector = begin + _pipe = gleam_erlang_ffi:new_selector(), + gleam@erlang@process:selecting(_pipe, Parent_subject, fun(X) -> X end) + end, + fun(Test_event@1) -> case Test_event@1 of + {end_test_run, _} -> + gleam@erlang@process:send( + Subject, + {event_handler_message, Test_event@1, Parent_subject} + ), + Num_failed@1 = gleam_erlang_ffi:select(Selector), + case Num_failed@1 > 0 of + true -> + erlang:halt(1); + + false -> + erlang:halt(0) + end; + + _ -> + gleam@erlang@process:send( + Subject, + {event_handler_message, Test_event@1, Parent_subject} + ) + end end. diff --git a/aoc2023/build/packages/adglent/src/showtime@internal@erlang@module_handler.erl b/aoc2023/build/packages/adglent/src/showtime@internal@erlang@module_handler.erl new file mode 100644 index 0000000..a6959f5 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@internal@erlang@module_handler.erl @@ -0,0 +1,53 @@ +-module(showtime@internal@erlang@module_handler). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([start/5]). + +-spec start( + fun((showtime@internal@common@test_suite:test_event()) -> nil), + fun((showtime@internal@common@test_suite:test_module()) -> showtime@internal@common@test_suite:test_suite()), + fun((showtime@internal@common@test_suite:test_suite(), fun((showtime@internal@common@test_suite:test_event()) -> nil), list(binary()), showtime@internal@common@cli:capture()) -> nil), + list(binary()), + showtime@internal@common@cli:capture() +) -> fun((showtime@internal@common@test_suite:test_module()) -> nil). +start( + Test_event_handler, + Test_function_collector, + Run_test_suite, + Ignore_tags, + Capture +) -> + _assert_subject = gleam@otp@actor:start( + nil, + fun(Module, State) -> + gleam@erlang@process:start( + fun() -> + Test_suite = Test_function_collector(Module), + Test_event_handler({start_test_suite, Module}), + Run_test_suite( + Test_suite, + Test_event_handler, + Ignore_tags, + Capture + ), + Test_event_handler({end_test_suite, Module}) + end, + false + ), + {continue, State, none} + end + ), + {ok, Subject} = case _assert_subject of + {ok, _} -> _assert_subject; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail, + module => <<"showtime/internal/erlang/module_handler"/utf8>>, + function => <<"start"/utf8>>, + line => 23}) + end, + fun(Test_module) -> + gleam@erlang@process:send(Subject, Test_module), + nil + end. diff --git a/aoc2023/build/packages/adglent/src/showtime@internal@erlang@runner.erl b/aoc2023/build/packages/adglent/src/showtime@internal@erlang@runner.erl new file mode 100644 index 0000000..702fb75 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@internal@erlang@runner.erl @@ -0,0 +1,46 @@ +-module(showtime@internal@erlang@runner). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([run_test/4, run_test_suite/4]). + +-spec run_test( + binary(), + binary(), + list(binary()), + showtime@internal@common@cli:capture() +) -> {ok, showtime@internal@common@test_result:test_return()} | + {error, showtime@internal@common@test_result:exception()}. +run_test(Module_name, Test_name, Ignore_tags, Capture) -> + Result = showtime_ffi:run_test( + erlang:binary_to_atom(Module_name), + erlang:binary_to_atom(Test_name), + Ignore_tags, + Capture + ), + Result. + +-spec run_test_suite( + showtime@internal@common@test_suite:test_suite(), + fun((showtime@internal@common@test_suite:test_event()) -> nil), + list(binary()), + showtime@internal@common@cli:capture() +) -> nil. +run_test_suite(Test_suite, Test_event_handler, Ignore_tags, Capture) -> + _pipe = erlang:element(3, Test_suite), + gleam@list:each( + _pipe, + fun(Test) -> + Test_event_handler( + {start_test, erlang:element(2, Test_suite), Test} + ), + Result = run_test( + erlang:element(2, erlang:element(2, Test_suite)), + erlang:element(2, Test), + Ignore_tags, + Capture + ), + Test_event_handler( + {end_test, erlang:element(2, Test_suite), Test, Result} + ) + end + ). diff --git a/aoc2023/build/packages/adglent/src/showtime@internal@reports@compare.erl b/aoc2023/build/packages/adglent/src/showtime@internal@reports@compare.erl new file mode 100644 index 0000000..d2969b2 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@internal@reports@compare.erl @@ -0,0 +1,61 @@ +-module(showtime@internal@reports@compare). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([compare/2]). + +-spec compare(gleam@dynamic:dynamic_(), gleam@dynamic:dynamic_()) -> {binary(), + binary()}. +compare(Expected, Got) -> + Expected_as_list = begin + _pipe = Expected, + (gleam@dynamic:list(fun gleam@dynamic:dynamic/1))(_pipe) + end, + Got_as_list = begin + _pipe@1 = Got, + (gleam@dynamic:list(fun gleam@dynamic:dynamic/1))(_pipe@1) + end, + Expected_as_string = begin + _pipe@2 = Expected, + gleam@dynamic:string(_pipe@2) + end, + Got_as_string = begin + _pipe@3 = Got, + gleam@dynamic:string(_pipe@3) + end, + case {Expected_as_list, Got_as_list, Expected_as_string, Got_as_string} of + {{ok, Expected_list}, {ok, Got_list}, _, _} -> + Comparison = begin + _pipe@4 = gap:compare_lists(Expected_list, Got_list), + _pipe@5 = gap@styling:from_comparison(_pipe@4), + _pipe@6 = gap@styling:highlight( + _pipe@5, + fun showtime@internal@reports@styles:expected_highlight/1, + fun showtime@internal@reports@styles:got_highlight/1, + fun(Item) -> Item end + ), + gap@styling:to_styled_comparison(_pipe@6) + end, + {erlang:element(2, Comparison), erlang:element(3, Comparison)}; + + {_, _, {ok, Expected_string}, {ok, Got_string}} -> + Comparison@1 = begin + _pipe@7 = gap:compare_strings(Expected_string, Got_string), + _pipe@8 = gap@styling:from_comparison(_pipe@7), + _pipe@9 = gap@styling:highlight( + _pipe@8, + fun showtime@internal@reports@styles:expected_highlight/1, + fun showtime@internal@reports@styles:got_highlight/1, + fun(Item@1) -> Item@1 end + ), + gap@styling:to_styled_comparison(_pipe@9) + end, + {erlang:element(2, Comparison@1), erlang:element(3, Comparison@1)}; + + {_, _, _, _} -> + {showtime@internal@reports@styles:expected_highlight( + gleam@string:inspect(Expected) + ), + showtime@internal@reports@styles:got_highlight( + gleam@string:inspect(Got) + )} + end. diff --git a/aoc2023/build/packages/adglent/src/showtime@internal@reports@formatter.erl b/aoc2023/build/packages/adglent/src/showtime@internal@reports@formatter.erl new file mode 100644 index 0000000..faea091 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@internal@reports@formatter.erl @@ -0,0 +1,749 @@ +-module(showtime@internal@reports@formatter). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([create_test_report/1]). +-export_type([glee_unit_assertion_type/0, module_and_test/0, unified_error/0]). + +-type glee_unit_assertion_type() :: {glee_unit_assert_equal, binary()} | + {glee_unit_assert_not_equal, binary()} | + {glee_unit_assert_match, binary()}. + +-type module_and_test() :: {module_and_test_run, + binary(), + showtime@internal@common@test_suite:test_run()}. + +-type unified_error() :: {unified_error, + gleam@option:option(showtime@tests@meta:meta()), + binary(), + binary(), + binary(), + binary(), + gleam@option:option(integer()), + list(showtime@internal@common@test_result:trace())}. + +-spec erlang_error_to_unified( + list(showtime@internal@common@test_result:reason_detail()), + glee_unit_assertion_type(), + list(showtime@internal@common@test_result:trace()) +) -> unified_error(). +erlang_error_to_unified(Error_details, Assertion_type, Stacktrace) -> + _pipe = Error_details, + gleam@list:fold( + _pipe, + {unified_error, + none, + <<"not_set"/utf8>>, + erlang:element(2, Assertion_type), + <<""/utf8>>, + <<""/utf8>>, + none, + Stacktrace}, + fun(Unified, Reason) -> case Reason of + {expression, Expression} -> + erlang:setelement(3, Unified, Expression); + + {expected, Value} -> + case Assertion_type of + {glee_unit_assert_equal, _} -> + erlang:setelement( + 5, + Unified, + showtime@internal@reports@styles:expected_highlight( + gleam@string:inspect(Value) + ) + ); + + _ -> + Unified + end; + + {value, Value@1} -> + case Assertion_type of + {glee_unit_assert_not_equal, _} -> + erlang:setelement( + 6, + erlang:setelement( + 5, + Unified, + <<(showtime@internal@reports@styles:not_style( + <<"not "/utf8>> + ))/binary, + (gleam@string:inspect(Value@1))/binary>> + ), + showtime@internal@reports@styles:got_highlight( + gleam@string:inspect(Value@1) + ) + ); + + _ -> + erlang:setelement( + 6, + Unified, + showtime@internal@reports@styles:got_highlight( + gleam@string:inspect(Value@1) + ) + ) + end; + + {pattern, Pattern} -> + case Pattern of + <<"{ ok , _ }"/utf8>> -> + erlang:setelement( + 5, + Unified, + showtime@internal@reports@styles:expected_highlight( + <<"Ok(_)"/utf8>> + ) + ); + + <<"{ error , _ }"/utf8>> -> + erlang:setelement( + 5, + Unified, + showtime@internal@reports@styles:expected_highlight( + <<"Error(_)"/utf8>> + ) + ); + + _ -> + Unified + end; + + _ -> + Unified + end end + ). + +-spec gleam_error_to_unified( + showtime@internal@common@test_result:gleam_error_detail(), + list(showtime@internal@common@test_result:trace()) +) -> unified_error(). +gleam_error_to_unified(Gleam_error, Stacktrace) -> + case Gleam_error of + {let_assert, _, _, _, _, Value} -> + Result = gleam@dynamic:unsafe_coerce(Value), + {error, Assertion} = case Result of + {error, _} -> Result; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail, + module => <<"showtime/internal/reports/formatter"/utf8>>, + function => <<"gleam_error_to_unified"/utf8>>, + line => 293}) + end, + case Assertion of + {eq, Got, Expected, Meta} -> + {Expected@1, Got@1} = showtime@internal@reports@compare:compare( + Expected, + Got + ), + {unified_error, + Meta, + <<"assert"/utf8>>, + <<"Assert equal"/utf8>>, + Expected@1, + Got@1, + none, + Stacktrace}; + + {not_eq, Got@2, Expected@2, Meta@1} -> + {unified_error, + Meta@1, + <<"assert"/utf8>>, + <<"Assert not equal"/utf8>>, + <<(showtime@internal@reports@styles:not_style( + <<"not "/utf8>> + ))/binary, + (gleam@string:inspect(Expected@2))/binary>>, + gleam@string:inspect(Got@2), + none, + Stacktrace}; + + {is_ok, Got@3, Meta@2} -> + {unified_error, + Meta@2, + <<"assert"/utf8>>, + <<"Assert is Ok"/utf8>>, + showtime@internal@reports@styles:expected_highlight( + <<"Ok(_)"/utf8>> + ), + showtime@internal@reports@styles:got_highlight( + gleam@string:inspect(Got@3) + ), + none, + Stacktrace}; + + {is_error, Got@4, Meta@3} -> + {unified_error, + Meta@3, + <<"assert"/utf8>>, + <<"Assert is Ok"/utf8>>, + showtime@internal@reports@styles:expected_highlight( + <<"Error(_)"/utf8>> + ), + showtime@internal@reports@styles:got_highlight( + gleam@string:inspect(Got@4) + ), + none, + Stacktrace}; + + {fail, Meta@4} -> + {unified_error, + Meta@4, + <<"assert"/utf8>>, + <<"Assert is Ok"/utf8>>, + showtime@internal@reports@styles:got_highlight( + <<"should.fail()"/utf8>> + ), + showtime@internal@reports@styles:got_highlight( + <<"N/A - test always expected to fail"/utf8>> + ), + none, + Stacktrace} + end + end. + +-spec format_reason(unified_error(), binary(), binary(), list(binary())) -> list(list(showtime@internal@reports@table:col())). +format_reason(Error, Module, Function, Output_buffer) -> + Meta@1 = case erlang:element(2, Error) of + {some, Meta} -> + {some, + [{align_right, + {styled_content, + showtime@internal@reports@styles:heading_style( + <<"Description"/utf8>> + )}, + 2}, + {separator, <<": "/utf8>>}, + {align_left, {content, erlang:element(2, Meta)}, 0}]}; + + none -> + none + end, + Stacktrace = begin + _pipe = erlang:element(8, Error), + gleam@list:map(_pipe, fun(Trace) -> case Trace of + {trace, Function@1, _, _} when Function@1 =:= <<""/utf8>> -> + <<"(anonymous)"/utf8>>; + + {trace_module, Module@1, Function@2, _, _} when Function@2 =:= <<""/utf8>> -> + <<<<Module@1/binary, "."/utf8>>/binary, + "(anonymous)"/utf8>>; + + {trace, Function@3, _, _} -> + Function@3; + + {trace_module, Module@2, Function@4, _, _} -> + <<<<Module@2/binary, "."/utf8>>/binary, + Function@4/binary>> + end end) + end, + Stacktrace_rows = case Stacktrace of + [] -> + []; + + [First | Rest] -> + First_row = {some, + [{align_right, + {styled_content, + showtime@internal@reports@styles:heading_style( + <<"Stacktrace"/utf8>> + )}, + 2}, + {separator, <<": "/utf8>>}, + {align_left, + {styled_content, + showtime@internal@reports@styles:stacktrace_style( + First + )}, + 0}]}, + Rest_rows = begin + _pipe@1 = Rest, + gleam@list:map( + _pipe@1, + fun(Row) -> + {some, + [{align_right, {content, <<""/utf8>>}, 2}, + {separator, <<" "/utf8>>}, + {align_left, + {styled_content, + showtime@internal@reports@styles:stacktrace_style( + Row + )}, + 0}]} + end + ) + end, + [First_row | Rest_rows] + end, + Output_rows = case begin + _pipe@2 = Output_buffer, + _pipe@3 = gleam@list:reverse(_pipe@2), + gleam@list:map( + _pipe@3, + fun(Row@1) -> gleam@string:trim_right(Row@1) end + ) + end of + [] -> + []; + + [First@1 | Rest@1] -> + First_row@1 = {some, + [{align_right, + {styled_content, + showtime@internal@reports@styles:heading_style( + <<"Output"/utf8>> + )}, + 2}, + {separator, <<": "/utf8>>}, + {align_left_overflow, + {styled_content, + showtime@internal@reports@styles:stacktrace_style( + First@1 + )}, + 0}]}, + Rest_rows@1 = begin + _pipe@4 = Rest@1, + gleam@list:map( + _pipe@4, + fun(Row@2) -> + {some, + [{align_right, {content, <<""/utf8>>}, 2}, + {separator, <<" "/utf8>>}, + {align_left_overflow, + {styled_content, + showtime@internal@reports@styles:stacktrace_style( + Row@2 + )}, + 0}]} + end + ) + end, + [First_row@1 | Rest_rows@1] + end, + Line@1 = begin + _pipe@5 = erlang:element(7, Error), + _pipe@6 = gleam@option:map( + _pipe@5, + fun(Line) -> <<":"/utf8, (gleam@int:to_string(Line))/binary>> end + ), + gleam@option:unwrap(_pipe@6, <<""/utf8>>) + end, + Arrow = <<(gleam@string:join( + gleam@list:repeat( + <<"-"/utf8>>, + (gleam@string:length(Module) + 1) + ((gleam@string:length( + Function + ) + + gleam@string:length(Line@1)) + div 2) + ), + <<""/utf8>> + ))/binary, + "⌄"/utf8>>, + Standard_table_rows = [{some, + [{align_right, + {styled_content, + showtime@internal@reports@styles:error_style( + <<"Failed"/utf8>> + )}, + 2}, + {separator, <<": "/utf8>>}, + {align_left, {content, Arrow}, 0}]}, + {some, + [{align_right, + {styled_content, + showtime@internal@reports@styles:heading_style( + <<"Test"/utf8>> + )}, + 2}, + {separator, <<": "/utf8>>}, + {align_left, + {styled_content, + <<<<Module/binary, "."/utf8>>/binary, + (showtime@internal@reports@styles:function_style( + <<Function/binary, Line@1/binary>> + ))/binary>>}, + 0}]}, + Meta@1, + {some, + [{align_right, + {styled_content, + showtime@internal@reports@styles:heading_style( + <<"Expected"/utf8>> + )}, + 2}, + {separator, <<": "/utf8>>}, + {align_left_overflow, + {styled_content, erlang:element(5, Error)}, + 0}]}, + {some, + [{align_right, + {styled_content, + showtime@internal@reports@styles:heading_style( + <<"Got"/utf8>> + )}, + 2}, + {separator, <<": "/utf8>>}, + {align_left_overflow, + {styled_content, erlang:element(6, Error)}, + 0}]}], + _pipe@7 = Standard_table_rows, + _pipe@8 = gleam@list:append(_pipe@7, Stacktrace_rows), + _pipe@9 = gleam@list:append(_pipe@8, Output_rows), + _pipe@10 = gleam@list:append( + _pipe@9, + [{some, + [{align_right, {content, <<""/utf8>>}, 0}, + {align_right, {content, <<""/utf8>>}, 0}, + {align_right, {content, <<""/utf8>>}, 0}]}] + ), + gleam@list:filter_map( + _pipe@10, + fun(Row@3) -> gleam@option:to_result(Row@3, nil) end + ). + +-spec create_test_report( + gleam@map:map_(binary(), gleam@map:map_(binary(), showtime@internal@common@test_suite:test_run())) +) -> {binary(), integer()}. +create_test_report(Test_results) -> + All_test_runs = begin + _pipe = Test_results, + _pipe@1 = gleam@map:values(_pipe), + gleam@list:flat_map(_pipe@1, fun gleam@map:values/1) + end, + Failed_test_runs = begin + _pipe@2 = Test_results, + _pipe@3 = gleam@map:to_list(_pipe@2), + gleam@list:flat_map( + _pipe@3, + fun(Entry) -> + {Module_name, Test_module_results} = Entry, + _pipe@4 = Test_module_results, + _pipe@5 = gleam@map:values(_pipe@4), + gleam@list:filter_map(_pipe@5, fun(Test_run) -> case Test_run of + {completed_test_run, _, _, Result} -> + case Result of + {error, _} -> + {ok, + {module_and_test_run, + Module_name, + Test_run}}; + + {ok, {ignored, _}} -> + {error, nil}; + + {ok, _} -> + {error, nil} + end; + + _ -> + _pipe@6 = Test_run, + gleam@io:debug(_pipe@6), + {error, nil} + end end) + end + ) + end, + Ignored_test_runs = begin + _pipe@7 = Test_results, + _pipe@8 = gleam@map:to_list(_pipe@7), + gleam@list:flat_map( + _pipe@8, + fun(Entry@1) -> + {Module_name@1, Test_module_results@1} = Entry@1, + _pipe@9 = Test_module_results@1, + _pipe@10 = gleam@map:values(_pipe@9), + gleam@list:filter_map( + _pipe@10, + fun(Test_run@1) -> case Test_run@1 of + {completed_test_run, Test_function, _, Result@1} -> + case Result@1 of + {ok, {ignored, Reason}} -> + {ok, + {<<<<Module_name@1/binary, + "."/utf8>>/binary, + (erlang:element( + 2, + Test_function + ))/binary>>, + Reason}}; + + _ -> + {error, nil} + end; + + _ -> + {error, nil} + end end + ) + end + ) + end, + Failed_tests_report = begin + _pipe@11 = Failed_test_runs, + _pipe@12 = gleam@list:filter_map( + _pipe@11, + fun(Module_and_test_run) -> + case erlang:element(3, Module_and_test_run) of + {completed_test_run, Test_function@1, _, Result@2} -> + case Result@2 of + {error, Exception} -> + case erlang:element(3, Exception) of + {assert_equal, Reason_details} -> + {ok, + format_reason( + erlang_error_to_unified( + Reason_details, + {glee_unit_assert_equal, + <<"Assert equal"/utf8>>}, + erlang:element( + 2, + erlang:element( + 4, + Exception + ) + ) + ), + erlang:element( + 2, + Module_and_test_run + ), + erlang:element( + 2, + Test_function@1 + ), + erlang:element(5, Exception) + )}; + + {assert_not_equal, Reason_details@1} -> + {ok, + format_reason( + erlang_error_to_unified( + Reason_details@1, + {glee_unit_assert_not_equal, + <<"Assert not equal"/utf8>>}, + erlang:element( + 2, + erlang:element( + 4, + Exception + ) + ) + ), + erlang:element( + 2, + Module_and_test_run + ), + erlang:element( + 2, + Test_function@1 + ), + erlang:element(5, Exception) + )}; + + {assert_match, Reason_details@2} -> + {ok, + format_reason( + erlang_error_to_unified( + Reason_details@2, + {glee_unit_assert_match, + <<"Assert match"/utf8>>}, + erlang:element( + 2, + erlang:element( + 4, + Exception + ) + ) + ), + erlang:element( + 2, + Module_and_test_run + ), + erlang:element( + 2, + Test_function@1 + ), + erlang:element(5, Exception) + )}; + + {gleam_error, Reason@1} -> + {ok, + format_reason( + gleam_error_to_unified( + Reason@1, + erlang:element( + 2, + erlang:element( + 4, + Exception + ) + ) + ), + erlang:element( + 2, + Module_and_test_run + ), + erlang:element( + 2, + Test_function@1 + ), + erlang:element(5, Exception) + )}; + + {gleam_assert, Value, Line_no} -> + {ok, + format_reason( + {unified_error, + none, + <<"gleam assert"/utf8>>, + <<"Assert failed"/utf8>>, + <<"Patterns should match"/utf8>>, + showtime@internal@reports@styles:error_style( + gleam@string:inspect( + Value + ) + ), + {some, Line_no}, + erlang:element( + 2, + erlang:element( + 4, + Exception + ) + )}, + erlang:element( + 2, + Module_and_test_run + ), + erlang:element( + 2, + Test_function@1 + ), + erlang:element(5, Exception) + )}; + + {generic_exception, Value@1} -> + {ok, + format_reason( + {unified_error, + none, + <<"generic exception"/utf8>>, + <<"Test function threw an exception"/utf8>>, + <<"Exception in test function"/utf8>>, + showtime@internal@reports@styles:error_style( + gleam@string:inspect( + Value@1 + ) + ), + none, + erlang:element( + 2, + erlang:element( + 4, + Exception + ) + )}, + erlang:element( + 2, + Module_and_test_run + ), + erlang:element( + 2, + Test_function@1 + ), + erlang:element(5, Exception) + )}; + + Other -> + gleam@io:println( + <<"Other: "/utf8, + (gleam@string:inspect(Other))/binary>> + ), + erlang:error(#{gleam_error => panic, + message => <<"panic expression evaluated"/utf8>>, + module => <<"showtime/internal/reports/formatter"/utf8>>, + function => <<"create_test_report"/utf8>>, + line => 178}), + {error, nil} + end; + + _ -> + {error, nil} + end; + + _ -> + {error, nil} + end + end + ), + gleam@list:fold( + _pipe@12, + [], + fun(Rows, Test_rows) -> gleam@list:append(Rows, Test_rows) end + ) + end, + All_test_execution_time_reports = begin + _pipe@13 = All_test_runs, + gleam@list:filter_map(_pipe@13, fun(Test_run@2) -> case Test_run@2 of + {completed_test_run, Test_function@2, Total_time, _} -> + {ok, + <<<<<<(erlang:element(2, Test_function@2))/binary, + ": "/utf8>>/binary, + (gleam@int:to_string(Total_time))/binary>>/binary, + " ms"/utf8>>}; + + _ -> + {error, nil} + end end) + end, + _ = begin + _pipe@14 = All_test_execution_time_reports, + gleam@string:join(_pipe@14, <<"\n"/utf8>>) + end, + All_tests_count = begin + _pipe@15 = All_test_runs, + gleam@list:length(_pipe@15) + end, + Ignored_tests_count = begin + _pipe@16 = Ignored_test_runs, + gleam@list:length(_pipe@16) + end, + Failed_tests_count = begin + _pipe@17 = Failed_test_runs, + gleam@list:length(_pipe@17) + end, + Passed = showtime@internal@reports@styles:passed_style( + <<(gleam@int:to_string( + (All_tests_count - Failed_tests_count) - Ignored_tests_count + ))/binary, + " passed"/utf8>> + ), + Failed = showtime@internal@reports@styles:failed_style( + <<(gleam@int:to_string(Failed_tests_count))/binary, " failed"/utf8>> + ), + Ignored = case Ignored_tests_count of + 0 -> + <<""/utf8>>; + + _ -> + <<", "/utf8, + (showtime@internal@reports@styles:ignored_style( + <<(gleam@int:to_string(Ignored_tests_count))/binary, + " ignored"/utf8>> + ))/binary>> + end, + Failed_tests_table = begin + _pipe@18 = {table, none, Failed_tests_report}, + _pipe@19 = showtime@internal@reports@table:align_table(_pipe@18), + showtime@internal@reports@table:to_string(_pipe@19) + end, + Test_report = <<<<<<<<<<<<"\n"/utf8, Failed_tests_table/binary>>/binary, + "\n"/utf8>>/binary, + Passed/binary>>/binary, + ", "/utf8>>/binary, + Failed/binary>>/binary, + Ignored/binary>>, + {Test_report, Failed_tests_count}. diff --git a/aoc2023/build/packages/adglent/src/showtime@internal@reports@styles.erl b/aoc2023/build/packages/adglent/src/showtime@internal@reports@styles.erl new file mode 100644 index 0000000..ec6230c --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@internal@reports@styles.erl @@ -0,0 +1,93 @@ +-module(showtime@internal@reports@styles). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([not_style/1, module_style/1, heading_style/1, stacktrace_style/1, failed_style/1, error_style/1, got_highlight/1, passed_style/1, expected_highlight/1, ignored_style/1, function_style/1, strip_style/1]). + +-spec not_style(binary()) -> binary(). +not_style(Text) -> + gleam_community@ansi:bold(Text). + +-spec module_style(binary()) -> binary(). +module_style(Text) -> + gleam_community@ansi:cyan(Text). + +-spec heading_style(binary()) -> binary(). +heading_style(Text) -> + gleam_community@ansi:cyan(Text). + +-spec stacktrace_style(binary()) -> binary(). +stacktrace_style(Text) -> + Text. + +-spec bold_red(binary()) -> binary(). +bold_red(Text) -> + gleam_community@ansi:bold(gleam_community@ansi:red(Text)). + +-spec failed_style(binary()) -> binary(). +failed_style(Text) -> + bold_red(Text). + +-spec error_style(binary()) -> binary(). +error_style(Text) -> + bold_red(Text). + +-spec got_highlight(binary()) -> binary(). +got_highlight(Text) -> + bold_red(Text). + +-spec bold_green(binary()) -> binary(). +bold_green(Text) -> + gleam_community@ansi:bold(gleam_community@ansi:green(Text)). + +-spec passed_style(binary()) -> binary(). +passed_style(Text) -> + bold_green(Text). + +-spec expected_highlight(binary()) -> binary(). +expected_highlight(Text) -> + bold_green(Text). + +-spec bold_yellow(binary()) -> binary(). +bold_yellow(Text) -> + gleam_community@ansi:bold(gleam_community@ansi:yellow(Text)). + +-spec ignored_style(binary()) -> binary(). +ignored_style(Text) -> + bold_yellow(Text). + +-spec bold_cyan(binary()) -> binary(). +bold_cyan(Text) -> + gleam_community@ansi:bold(gleam_community@ansi:cyan(Text)). + +-spec function_style(binary()) -> binary(). +function_style(Text) -> + bold_cyan(Text). + +-spec strip_style(binary()) -> binary(). +strip_style(Text) -> + {New_text, _} = begin + _pipe = Text, + _pipe@1 = gleam@string:to_graphemes(_pipe), + gleam@list:fold( + _pipe@1, + {<<""/utf8>>, false}, + fun(Acc, Char) -> + {Str, Removing} = Acc, + Bit_char = gleam_stdlib:identity(Char), + case {Bit_char, Removing} of + {<<16#1b>>, _} -> + {Str, true}; + + {<<16#6d>>, true} -> + {Str, false}; + + {_, true} -> + {Str, true}; + + {_, false} -> + {<<Str/binary, Char/binary>>, false} + end + end + ) + end, + New_text. diff --git a/aoc2023/build/packages/adglent/src/showtime@internal@reports@table.erl b/aoc2023/build/packages/adglent/src/showtime@internal@reports@table.erl new file mode 100644 index 0000000..35dbba3 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@internal@reports@table.erl @@ -0,0 +1,229 @@ +-module(showtime@internal@reports@table). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([to_string/1, align_table/1]). +-export_type([content/0, col/0, table/0]). + +-type content() :: {content, binary()} | {styled_content, binary()}. + +-type col() :: {align_right, content(), integer()} | + {align_left, content(), integer()} | + {align_right_overflow, content(), integer()} | + {align_left_overflow, content(), integer()} | + {separator, binary()} | + {aligned, binary()}. + +-type table() :: {table, gleam@option:option(binary()), list(list(col()))}. + +-spec to_string(table()) -> binary(). +to_string(Table) -> + Rows = begin + _pipe = erlang:element(3, Table), + _pipe@3 = gleam@list:map(_pipe, fun(Row) -> _pipe@1 = Row, + _pipe@2 = gleam@list:filter_map(_pipe@1, fun(Col) -> case Col of + {separator, Char} -> + {ok, Char}; + + {aligned, Content} -> + {ok, Content}; + + _ -> + {error, nil} + end end), + gleam@string:join(_pipe@2, <<""/utf8>>) end), + gleam@string:join(_pipe@3, <<"\n"/utf8>>) + end, + Header@1 = begin + _pipe@4 = erlang:element(2, Table), + _pipe@5 = gleam@option:map( + _pipe@4, + fun(Header) -> <<Header/binary, "\n"/utf8>> end + ), + gleam@option:unwrap(_pipe@5, <<""/utf8>>) + end, + <<Header@1/binary, Rows/binary>>. + +-spec pad_left(binary(), integer(), binary()) -> binary(). +pad_left(Str, Num, Char) -> + Padding = begin + _pipe = gleam@list:repeat(Char, Num), + gleam@string:join(_pipe, <<""/utf8>>) + end, + <<Padding/binary, Str/binary>>. + +-spec pad_right(binary(), integer(), binary()) -> binary(). +pad_right(Str, Num, Char) -> + Padding = begin + _pipe = gleam@list:repeat(Char, Num), + gleam@string:join(_pipe, <<""/utf8>>) + end, + <<Str/binary, Padding/binary>>. + +-spec align_table(table()) -> table(). +align_table(Table) -> + Cols = begin + _pipe = erlang:element(3, Table), + gleam@list:transpose(_pipe) + end, + Col_width = begin + _pipe@1 = Cols, + gleam@list:map(_pipe@1, fun(Col) -> _pipe@2 = Col, + _pipe@3 = gleam@list:map( + _pipe@2, + fun(Content) -> case Content of + {align_right, {content, Unstyled}, _} -> + Unstyled; + + {align_right, {styled_content, Styled}, _} -> + showtime@internal@reports@styles:strip_style( + Styled + ); + + {align_left, {content, Unstyled@1}, _} -> + Unstyled@1; + + {align_left, {styled_content, Styled@1}, _} -> + showtime@internal@reports@styles:strip_style( + Styled@1 + ); + + {align_left_overflow, _, _} -> + <<""/utf8>>; + + {align_right_overflow, _, _} -> + <<""/utf8>>; + + {separator, Char} -> + Char; + + {aligned, Content@1} -> + Content@1 + end end + ), + gleam@list:fold( + _pipe@3, + 0, + fun(Max, Str) -> + gleam@int:max(Max, gleam@string:length(Str)) + end + ) end) + end, + Aligned_col = begin + _pipe@4 = Cols, + _pipe@5 = gleam@list:zip(_pipe@4, Col_width), + gleam@list:map( + _pipe@5, + fun(Col_and_width) -> + {Col@1, Width} = Col_and_width, + _pipe@6 = Col@1, + gleam@list:map(_pipe@6, fun(Content@2) -> case Content@2 of + {align_right, {content, Unstyled@2}, Margin} -> + {aligned, + pad_left( + Unstyled@2, + (Width + Margin) - gleam@string:length( + Unstyled@2 + ), + <<" "/utf8>> + )}; + + {align_right, {styled_content, Styled@2}, Margin@1} -> + {aligned, + pad_left( + Styled@2, + (Width + Margin@1) - gleam@string:length( + showtime@internal@reports@styles:strip_style( + Styled@2 + ) + ), + <<" "/utf8>> + )}; + + {align_right_overflow, + {content, Unstyled@3}, + Margin@2} -> + {aligned, + pad_left( + Unstyled@3, + (Width + Margin@2) - gleam@string:length( + Unstyled@3 + ), + <<" "/utf8>> + )}; + + {align_right_overflow, + {styled_content, Styled@3}, + Margin@3} -> + {aligned, + pad_left( + Styled@3, + (Width + Margin@3) - gleam@string:length( + showtime@internal@reports@styles:strip_style( + Styled@3 + ) + ), + <<" "/utf8>> + )}; + + {align_left, {content, Unstyled@4}, Margin@4} -> + {aligned, + pad_right( + Unstyled@4, + (Width + Margin@4) - gleam@string:length( + Unstyled@4 + ), + <<" "/utf8>> + )}; + + {align_left, {styled_content, Styled@4}, Margin@5} -> + {aligned, + pad_right( + Styled@4, + (Width + Margin@5) - gleam@string:length( + showtime@internal@reports@styles:strip_style( + Styled@4 + ) + ), + <<" "/utf8>> + )}; + + {align_left_overflow, + {content, Unstyled@5}, + Margin@6} -> + {aligned, + pad_right( + Unstyled@5, + (Width + Margin@6) - gleam@string:length( + Unstyled@5 + ), + <<" "/utf8>> + )}; + + {align_left_overflow, + {styled_content, Styled@5}, + Margin@7} -> + {aligned, + pad_right( + Styled@5, + (Width + Margin@7) - gleam@string:length( + showtime@internal@reports@styles:strip_style( + Styled@5 + ) + ), + <<" "/utf8>> + )}; + + {separator, Char@1} -> + {separator, Char@1}; + + {aligned, Content@3} -> + {aligned, Content@3} + end end) + end + ) + end, + Aligned_rows = begin + _pipe@7 = Aligned_col, + gleam@list:transpose(_pipe@7) + end, + erlang:setelement(3, Table, Aligned_rows). diff --git a/aoc2023/build/packages/adglent/src/showtime@tests@meta.erl b/aoc2023/build/packages/adglent/src/showtime@tests@meta.erl new file mode 100644 index 0000000..c975467 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@tests@meta.erl @@ -0,0 +1,8 @@ +-module(showtime@tests@meta). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export_type([meta/0]). + +-type meta() :: {meta, binary(), list(binary())}. + + diff --git a/aoc2023/build/packages/adglent/src/showtime@tests@should.erl b/aoc2023/build/packages/adglent/src/showtime@tests@should.erl new file mode 100644 index 0000000..29906b4 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@tests@should.erl @@ -0,0 +1,143 @@ +-module(showtime@tests@should). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([evaluate/1, equal/2, equal_meta/3, not_equal/2, not_equal_meta/3, be_ok/1, be_ok_meta/2, be_error/1, be_error_meta/2, fail/0, fail_meta/1, be_true/1, be_true_meta/2, be_false/1, be_false_meta/2]). +-export_type([assertion/2]). + +-type assertion(MXP, MXQ) :: {eq, + MXP, + MXP, + gleam@option:option(showtime@tests@meta:meta())} | + {not_eq, MXP, MXP, gleam@option:option(showtime@tests@meta:meta())} | + {is_ok, + {ok, MXP} | {error, MXQ}, + gleam@option:option(showtime@tests@meta:meta())} | + {is_error, + {ok, MXP} | {error, MXQ}, + gleam@option:option(showtime@tests@meta:meta())} | + {fail, gleam@option:option(showtime@tests@meta:meta())}. + +-spec evaluate(assertion(any(), any())) -> nil. +evaluate(Assertion) -> + case Assertion of + {eq, A, B, _} -> + case A =:= B of + true -> + nil; + + false -> + showtime_ffi:gleam_error({error, Assertion}) + end; + + {not_eq, A@1, B@1, _} -> + case A@1 /= B@1 of + true -> + nil; + + false -> + showtime_ffi:gleam_error({error, Assertion}) + end; + + {is_ok, A@2, _} -> + case A@2 of + {ok, _} -> + nil; + + {error, _} -> + showtime_ffi:gleam_error({error, Assertion}) + end; + + {is_error, A@3, _} -> + case A@3 of + {error, _} -> + nil; + + {ok, _} -> + showtime_ffi:gleam_error({error, Assertion}) + end; + + {fail, _} -> + showtime_ffi:gleam_error({error, Assertion}) + end. + +-spec equal(MXR, MXR) -> nil. +equal(A, B) -> + evaluate({eq, A, B, none}). + +-spec equal_meta(MXT, MXT, showtime@tests@meta:meta()) -> nil. +equal_meta(A, B, Meta) -> + evaluate({eq, A, B, {some, Meta}}). + +-spec not_equal(MXV, MXV) -> nil. +not_equal(A, B) -> + evaluate({not_eq, A, B, none}). + +-spec not_equal_meta(MXX, MXX, showtime@tests@meta:meta()) -> nil. +not_equal_meta(A, B, Meta) -> + evaluate({not_eq, A, B, {some, Meta}}). + +-spec be_ok({ok, MXZ} | {error, any()}) -> MXZ. +be_ok(A) -> + evaluate({is_ok, A, none}), + {ok, Value} = case A of + {ok, _} -> A; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail, + module => <<"showtime/tests/should"/utf8>>, + function => <<"be_ok"/utf8>>, + line => 30}) + end, + Value. + +-spec be_ok_meta({ok, any()} | {error, any()}, showtime@tests@meta:meta()) -> nil. +be_ok_meta(A, Meta) -> + evaluate({is_ok, A, {some, Meta}}). + +-spec be_error({ok, any()} | {error, MYK}) -> MYK. +be_error(A) -> + evaluate({is_error, A, none}), + {error, Value} = case A of + {error, _} -> A; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail, + module => <<"showtime/tests/should"/utf8>>, + function => <<"be_error"/utf8>>, + line => 40}) + end, + Value. + +-spec be_error_meta({ok, any()} | {error, any()}, showtime@tests@meta:meta()) -> nil. +be_error_meta(A, Meta) -> + evaluate({is_error, A, {some, Meta}}). + +-spec fail() -> nil. +fail() -> + evaluate({fail, none}). + +-spec fail_meta(showtime@tests@meta:meta()) -> nil. +fail_meta(Meta) -> + evaluate({fail, {some, Meta}}). + +-spec be_true(boolean()) -> nil. +be_true(A) -> + _pipe = A, + equal(_pipe, true). + +-spec be_true_meta(boolean(), showtime@tests@meta:meta()) -> nil. +be_true_meta(A, Meta) -> + _pipe = A, + equal_meta(_pipe, true, Meta). + +-spec be_false(boolean()) -> nil. +be_false(A) -> + _pipe = A, + equal(_pipe, false). + +-spec be_false_meta(boolean(), showtime@tests@meta:meta()) -> nil. +be_false_meta(A, Meta) -> + _pipe = A, + equal_meta(_pipe, false, Meta). diff --git a/aoc2023/build/packages/adglent/src/showtime@tests@test.erl b/aoc2023/build/packages/adglent/src/showtime@tests@test.erl new file mode 100644 index 0000000..2f23b9f --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime@tests@test.erl @@ -0,0 +1,57 @@ +-module(showtime@tests@test). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([test/2, equal/3, not_equal/3, with_meta/2, be_ok/2, be_error/2, fail/1, be_true/2, be_false/2]). +-export_type([test/0, meta_should/1]). + +-type test() :: {test, showtime@tests@meta:meta(), fun(() -> nil)}. + +-type meta_should(NDO) :: {meta_should, + fun((NDO, NDO) -> nil), + fun((NDO, NDO) -> nil)}. + +-spec test(showtime@tests@meta:meta(), fun((showtime@tests@meta:meta()) -> nil)) -> test(). +test(Meta, Test_function) -> + {test, Meta, fun() -> Test_function(Meta) end}. + +-spec equal(NDT, NDT, showtime@tests@meta:meta()) -> nil. +equal(A, B, Meta) -> + gleam@io:debug(A), + gleam@io:debug(B), + showtime@tests@should:equal_meta(A, B, Meta). + +-spec not_equal(NDV, NDV, showtime@tests@meta:meta()) -> nil. +not_equal(A, B, Meta) -> + showtime@tests@should:equal_meta(A, B, Meta). + +-spec with_meta(showtime@tests@meta:meta(), fun((meta_should(any())) -> nil)) -> test(). +with_meta(Meta, Test_function) -> + {test, + Meta, + fun() -> + Test_function( + {meta_should, + fun(A, B) -> equal(A, B, Meta) end, + fun(A@1, B@1) -> not_equal(A@1, B@1, Meta) end} + ) + end}. + +-spec be_ok({ok, any()} | {error, any()}, showtime@tests@meta:meta()) -> nil. +be_ok(A, Meta) -> + showtime@tests@should:be_ok_meta(A, Meta). + +-spec be_error({ok, any()} | {error, any()}, showtime@tests@meta:meta()) -> nil. +be_error(A, Meta) -> + showtime@tests@should:be_error_meta(A, Meta). + +-spec fail(showtime@tests@meta:meta()) -> nil. +fail(Meta) -> + showtime@tests@should:fail_meta(Meta). + +-spec be_true(boolean(), showtime@tests@meta:meta()) -> nil. +be_true(A, Meta) -> + showtime@tests@should:be_true_meta(A, Meta). + +-spec be_false(boolean(), showtime@tests@meta:meta()) -> nil. +be_false(A, Meta) -> + showtime@tests@should:be_false_meta(A, Meta). diff --git a/aoc2023/build/packages/adglent/src/showtime_ffi.erl b/aoc2023/build/packages/adglent/src/showtime_ffi.erl new file mode 100644 index 0000000..3259623 --- /dev/null +++ b/aoc2023/build/packages/adglent/src/showtime_ffi.erl @@ -0,0 +1,187 @@ +-module(showtime_ffi). + +-export([run_test/4, functions/0, capture_output/1, gleam_error/1]). + +gleam_error(Value) -> + erlang:error(#{ + gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => Value, + module => <<"this/is/not/used"/utf8>>, + function => <<"gleam_error"/utf8>>, + % Not used + line => 0 + }). + +start_output_capture(Capture) -> + OldGroupLeader = group_leader(), + CapturePid = spawn(showtime_ffi, capture_output, [{[], {OldGroupLeader, Capture}}]), + group_leader(CapturePid, self()), + {CapturePid, OldGroupLeader}. + +stop_output_capture({CapturePid, OldGroupLeader}) -> + group_leader(OldGroupLeader, self()), + CapturePid ! {capture_done, self()}, + receive + Buffer -> + Buffer + end. + +capture_output({Buffer, {OldGroupLeader, Capture}}) -> + receive + {io_request, From, ReplyAs, {put_chars, unicode, BitString}} -> + case Capture of + yes -> + From ! {io_reply, ReplyAs, ok}, + capture_output({[BitString | Buffer], {OldGroupLeader, Capture}}); + mixed -> + OldGroupLeader ! {io_request, From, ReplyAs, {put_chars, unicode, BitString}}, + capture_output({[BitString | Buffer], {OldGroupLeader, Capture}}); + no -> + OldGroupLeader ! {io_request, From, ReplyAs, {put_chars, unicode, BitString}}, + capture_output({Buffer, {OldGroupLeader, Capture}}) + end; + {capture_done, SenderPid} -> + SenderPid ! Buffer; + OtherMessage -> + OldGroupLeader ! OtherMessage, + capture_output({Buffer, {OldGroupLeader, Capture}}) + end. + +run_test(Module, Function, IgnoreTags, Capture) -> + OutputCapture = start_output_capture(Capture), + try + Result = apply(Module, Function, []), + {ResultType, FinalResult} = + case Result of + {test, {meta, _Description, Tags}, TestFun} -> + case + lists:any( + fun(Tag) -> + lists:any(fun(IgnoreTag) -> IgnoreTag == Tag end, IgnoreTags) + end, + Tags + ) + of + true -> + {ignored, ignore}; + false -> + {test_function_return, TestFun()} + end; + DirectResult -> + {test_function_return, DirectResult} + end, + OutputCaptureBuffer = stop_output_capture(OutputCapture), + case ResultType of + ignored -> {ok, {ResultType, FinalResult}}; + _ -> {ok, {ResultType, FinalResult, OutputCaptureBuffer}} + end + catch + Class:Reason:Stacktrace -> + GleamReason = + case Reason of + {Assertion, ReasonList} -> + ErlangReasonList = + lists:map( + fun(ReasonDetail) -> + case ReasonDetail of + {line, LineNo} -> + {reason_line, LineNo}; + {expression, List} -> + {expression, list_to_binary(List)}; + {module, ModuleAtom} -> + {module, atom_to_binary(ModuleAtom)}; + {pattern, Pattern} -> + {pattern, list_to_binary(Pattern)}; + Other -> + Other + end + end, + ReasonList + ), + GleamAssertionType = + case Assertion of + assertEqual -> + assert_equal; + assertNotEqual -> + assert_not_equal; + assertMatch -> + assert_match; + OtherAssertionType -> + OtherAssertionType + end, + {GleamAssertionType, ErlangReasonList}; + #{ + function := GleamFunction, + gleam_error := GleamError, + line := Line, + message := Message, + module := GleamModule, + value := Value + } -> + case Value of + {error, {OkValue, _, _, _}} when OkValue == not_eq; OkValue == eq -> + {gleam_error, + {GleamError, GleamModule, GleamFunction, Line, Message, Value}}; + {error, {OkValue, _, _}} when OkValue == is_ok; OkValue == is_error -> + {gleam_error, + {GleamError, GleamModule, GleamFunction, Line, Message, Value}}; + {error, {OkValue, _}} when OkValue == fail -> + {gleam_error, + {GleamError, GleamModule, GleamFunction, Line, Message, Value}}; + _ -> + {gleam_assert, Value, Line} + end; + OtherReason -> + {generic_exception, OtherReason} + end, + GleamClass = + case Class of + error -> + erlang_error; + Other -> + Other + end, + GleamTraceList = + lists:map( + fun(Trace) -> + case Trace of + {ModuleName, FunctionName, Arity, ExtraInfoList} -> + {trace_module, atom_to_binary(ModuleName), + atom_to_binary(FunctionName), map_arity(Arity), + map_extra_info_list(ExtraInfoList)}; + {FunctionName, Arity, ExtraInfoList} -> + {trace, atom_to_binary(FunctionName), map_arity(Arity), + map_extra_info_list(ExtraInfoList)} + end + end, + Stacktrace + ), + OutputCaptureBufferCatch = stop_output_capture(OutputCapture), + {error, + {erlang_exception, GleamClass, GleamReason, {trace_list, GleamTraceList}, + OutputCaptureBufferCatch}} + end. + +map_extra_info_list(ExtraInfoList) -> + lists:map( + fun(ExtraInfo) -> + case ExtraInfo of + {file, FileNameList} -> {file, list_to_binary(FileNameList)}; + Other -> Other + end + end, + ExtraInfoList + ). + +map_arity(Arity) -> + if + is_list(Arity) -> + {arg_list, Arity}; + is_integer(Arity) -> + {num, Arity} + end. + +functions() -> + Funs = module_info(exports), + Funs. |