From 540c7b561b5dd772afe5c9d2ea004230cb7dc153 Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Tue, 6 Dec 2022 15:50:23 -0500 Subject: [PATCH] allow for dynamic part run output, catch assertion messages, add --allow-crash flag to run and run all commands --- README.md | 12 +++++- gleam.toml | 2 +- src/cmd/new.gleam | 4 +- src/cmd/run.gleam | 105 ++++++++++++++++++++++++++++++++++++---------- src/runners.gleam | 3 +- 5 files changed, 98 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index f26206d..468fe5d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ To add this library to your project run: `gleam add gladvent` and add `import gl This library provides 2 options to run your advent of code solvers: 1. The easy way: simply add `gladvent.main()` to the end of your project's `main` function. -2. Create your own `Map(Int, #(fn(String) -> Int, fn(String) -> Int))` and pass it to `gladvent.execute` +2. Create your own `Map(Int, #(fn(String) -> Dynamic, fn(String) -> Dynamic))` and pass it to `gladvent.execute` ## Available commands @@ -31,23 +31,31 @@ This project provides your application with 2 commands, `new` and `run`: - flags: - `--timeout`: `gleam run run --timeout={timeout in ms} a b c ...` - usage example: `gleam run run --timeout=1000 1 2` with timeout 1000 milliseconds and days 1 and 2, runs and prints the output of running the `run` function of `day_1.gleam` and `day_2.gleam` + - `--allow-crash`: runs days without the use of `rescue` functionality, rendering output text more verbose but also allowing for stacktraces to be printed + - usage example: `gleam run run 1 2 3 --allow-crash` - `run all`: run all registered days - format: `gleam run run all` - flags: - - `--timeout`: `gleam run run --timeout={timeout in ms} a b c ...` + - `--timeout`: `gleam run run all --timeout={timeout in ms}` - usage example: `gleam run run --timeout=1000 1 2` with timeout 1000 milliseconds and days 1 and 2, runs and prints the output of running the `run` function of `day_1.gleam` and `day_2.gleam` + - `--allow-crash`: runs days without the use of `rescue` functionality, rendering output text more verbose but also allowing for stacktraces to be printed + - usage example: `gleam run run all --allow-crash` _Note:_ +- due to how `gladvent` works, the `pt_1` and `pt_2` functions only need to return `Dynamic` when direction building a `RunnerMap` and using `gladvent.execute`, when using `gladvent.main` they can return anything. - the `new` command creates source files in `src/days/` and input files in the `input/` directory. - the `run` command expects input files to be in the `input/` directory. - using `gladvent.main` expects gleam day runners to be in `src/days/` +- any triggered `assert` will be captured and printed, for example: `error: assert - Assertion pattern match failed in module days/day_1 in function pt_1 at line 2 with value 2` +- any message in a `todo` will be captured and printed, for example: `error: todo - test in module days/day_1 in function pt_2 at line 7` ## Seeing help messages - To see available subcommands: `gleam run -- --help` - To see help for the `run` command: `gleam run run --help` +- To see help for the `run` command: `gleam run run all --help` - To see help for the `new` command: `gleam run new --help` ## General Workflow diff --git a/gleam.toml b/gleam.toml index 08c806d..5908135 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "gladvent" -version = "0.4.2" +version = "0.5.0" repository = { type = "github", user = "TanklesXL", repo = "gladvent" } description = "An Advent Of Code runner for gleam" licences = ["Apache-2.0"] diff --git a/src/cmd/new.gleam b/src/cmd/new.gleam index 392673c..d9e695a 100644 --- a/src/cmd/new.gleam +++ b/src/cmd/new.gleam @@ -95,11 +95,11 @@ fn do(day: Day) -> snag.Result(Nil) { create_files(day) } -const gleam_starter = "pub fn pt_1(input: String) -> Int { +const gleam_starter = "pub fn pt_1(input: String) { todo } -pub fn pt_2(input: String) -> Int { +pub fn pt_2(input: String) { todo } " diff --git a/src/cmd/run.gleam b/src/cmd/run.gleam index 1979624..d84fa1d 100644 --- a/src/cmd/run.gleam +++ b/src/cmd/run.gleam @@ -1,5 +1,5 @@ -import gleam/list import gleam/int +import gleam/list import gleam/result import gleam/string import snag.{Result, Snag} @@ -7,7 +7,6 @@ import gleam/erlang/file import gleam/erlang import gleam/erlang/charlist.{Charlist} import gleam/erlang/atom -import gleam/dynamic import parse.{Day} import gleam/map import cmd.{Ending, Endless} @@ -15,6 +14,8 @@ import glint.{CommandInput} import glint/flag import gleam import runners.{RunnerMap} +import gleam/dynamic.{Dynamic} +import gleam/option.{None, Option} type SolveErr { Undef @@ -38,7 +39,7 @@ fn err_to_snag(err: Err) -> Snag { } type RunResult = - gleam.Result(Int, SolveErr) + gleam.Result(Dynamic, SolveErr) type Direction { // Leading @@ -56,6 +57,7 @@ external fn do_trim(String, Direction, Charlist) -> String = fn do( day: Day, runners: RunnerMap, + allow_crash: Bool, ) -> gleam.Result(#(RunResult, RunResult), Err) { try #(pt_1, pt_2) = map.get(runners, day) @@ -68,15 +70,21 @@ fn do( |> file.read() |> result.map(string_trim(_, Both, "\n")) |> result.replace_error(FailedToReadInput(input_path)) - let pt_1 = - erlang.rescue(fn() { pt_1(input) }) - |> result.map_error(run_err_to_string) - - let pt_2 = - erlang.rescue(fn() { pt_2(input) }) - |> result.map_error(run_err_to_string) - Ok(#(pt_1, pt_2)) + case allow_crash { + True -> Ok(#(Ok(pt_1(input)), Ok(pt_2(input)))) + False -> { + let pt_1 = + fn() { pt_1(input) } + |> erlang.rescue + |> result.map_error(run_err_to_string) + let pt_2 = + fn() { pt_2(input) } + |> erlang.rescue + |> result.map_error(run_err_to_string) + Ok(#(pt_1, pt_2)) + } + } } fn crash_to_dyn(err: erlang.Crash) -> dynamic.Dynamic { @@ -85,23 +93,69 @@ fn crash_to_dyn(err: erlang.Crash) -> dynamic.Dynamic { } } +type GleamErr { + GleamErr( + gleam_error: atom.Atom, + module: String, + function: String, + line: Int, + message: String, + value: Option(Dynamic), + ) +} + +fn decode_gleam_err() { + dynamic.decode6( + GleamErr, + dynamic.field(atom.create_from_string("gleam_error"), atom.from_dynamic), + dynamic.field(atom.create_from_string("module"), dynamic.string), + dynamic.field(atom.create_from_string("function"), dynamic.string), + dynamic.field(atom.create_from_string("line"), dynamic.int), + dynamic.field(atom.create_from_string("message"), dynamic.string), + dynamic.any([ + dynamic.field( + atom.create_from_string("value"), + dynamic.optional(dynamic.dynamic), + ), + fn(_) { Ok(None) }, + ]), + ) +} + +fn gleam_err_to_string(g: GleamErr) -> String { + string.join( + [ + "error:", + atom.to_string(g.gleam_error), + "-", + g.message, + "in module", + g.module, + "in function", + g.function, + "at line", + int.to_string(g.line), + g.value + |> option.map(fn(val) { "with value " <> string.inspect(val) }) + |> option.unwrap(""), + ], + " ", + ) +} + fn run_err_to_string(err: erlang.Crash) -> SolveErr { let dyn = crash_to_dyn(err) - { - try m = dynamic.map(atom.from_dynamic, dynamic.dynamic)(dyn) - map.get(m, atom.create_from_string("message")) - |> result.replace_error([]) - |> result.then(dynamic.string) - } + decode_gleam_err()(dyn) + |> result.map(gleam_err_to_string) |> result.lazy_unwrap(fn() { - "run failed for some reason: " <> string.inspect(dyn) + "run failed for some reason: " <> string.inspect(err) }) |> RunFailed } fn run_res_to_string(res: RunResult) -> String { case res { - Ok(res) -> int.to_string(res) + Ok(res) -> string.inspect(res) Error(err) -> case err { Undef -> "function undefined" @@ -126,15 +180,21 @@ fn collect(x: #(Day, gleam.Result(#(RunResult, RunResult), Err))) -> String { } } +// ----- CLI ----- + fn timeout_flag() { flag.int("timeout", 0, "Run with specified timeout") } +fn allow_crash_flag() { + flag.bool("allow-crash", False, "Don't catch exceptions thrown by runners") +} + pub fn run_command(runners: RunnerMap) -> glint.Stub(Result(List(String))) { glint.Stub( path: ["run"], run: run(_, runners, False), - flags: [timeout_flag()], + flags: [timeout_flag(), allow_crash_flag()], description: "Run the specified days", ) } @@ -143,7 +203,7 @@ pub fn run_all_command(runners: RunnerMap) -> glint.Stub(Result(List(String))) { glint.Stub( path: ["run", "all"], run: run(_, runners, True), - flags: [timeout_flag()], + flags: [timeout_flag(), allow_crash_flag()], description: "Run all registered days", ) } @@ -154,6 +214,7 @@ fn run( run_all: Bool, ) -> Result(List(String)) { assert Ok(flag.I(timeout)) = flag.get(input.flags, timeout_flag().0) + assert Ok(flag.B(allow_crash)) = flag.get(input.flags, allow_crash_flag().0) try timing = case timeout { 0 -> Ok(Endless) @@ -175,7 +236,7 @@ fn run( } days - |> cmd.exec(timing, do(_, runners), Other, collect) + |> cmd.exec(timing, do(_, runners, allow_crash), Other, collect) |> Ok } diff --git a/src/runners.gleam b/src/runners.gleam index f505a67..d943ca5 100644 --- a/src/runners.gleam +++ b/src/runners.gleam @@ -6,13 +6,14 @@ import parse.{Day} import gleam/list import gleam/result import gleam +import gleam/dynamic.{Dynamic} pub const input_dir = "input/" pub const days_dir = "src/days/" pub type PartRunner = - fn(String) -> Int + fn(String) -> Dynamic pub type DayRunner = #(PartRunner, PartRunner)