-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathrunner.ex
165 lines (141 loc) · 5.52 KB
/
runner.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# Copyright 2022 Google LLC
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#
# Runner for Elixir Advent of Code solutions.
# Usage: elixir -r runner.ex day0/day0.exs day0/input*.txt
defmodule Runner do
defmodule Result do
defstruct outcome: :unknown, got: "", want: "", time_usec: 0
def ok?(%Result{outcome: :fail}), do: false
def ok?(%Result{}), do: true
@outcome_colors %{
success: :light_green_background,
fail: :light_red_background,
unknown: :light_yellow_background,
todo: :cyan_background
}
@outcome_signs %{success: "✅", fail: "❌", unknown: "❓", todo: "❗"}
def message(result) do
outcome = Atom.to_string(result.outcome) |> String.upcase()
sign = @outcome_signs[result.outcome]
background = @outcome_colors[result.outcome]
msg =
case result do
%Result{outcome: :success, got: got} -> "got #{got}"
%Result{outcome: :fail, got: got, want: want} -> "got #{got}, want #{want}"
%Result{outcome: :unknown, got: got} -> "got #{got}"
%Result{outcome: :todo, want: ""} -> "implement it"
%Result{outcome: :todo, want: want} -> "implement it, want #{want}"
end
IO.ANSI.format([sign, " ", background, :black, outcome, :reset, " ", msg])
end
end
@doc """
Parses argv as a command line and runs part1 and part2 of daymodule on each
file in argv, or standard input if no files are given or any file is "-".
Returns true if all parts match their expected values for all files,
otherwise returns false. If `--verbose` or `-v` is present in argv,
additional information like filenames, expected values, and timing will be
printed to standard error.
## Examples:
# Run against multiple files, print extra output
iex> Runner.main(Day0, ~w[--verbose input.example.txt input.actual.txt])
# Run against standard input, no extra output
iex> Runner.main(Day0, [])
"""
@spec main(module, [String.t()]) :: boolean
def main(daymodule, argv) do
{args, files} = OptionParser.parse!(argv, strict: [verbose: :boolean], aliases: [v: :verbose])
verbose = Keyword.get(args, :verbose, false)
files = if Enum.empty?(files), do: ["-"], else: files
Enum.all?(Enum.map(files, &run(daymodule, &1, verbose)))
end
@doc """
Reads input from file and passes it to :part1 and :part2 functions in
daymodule. Prints results. Also checks for a companion .expected file and
prints "SUCCESS" if the result matches, FAIL if it does not, "UNKNOWN" if the
expected value is not yet set, and "TODO" if the function returned :todo.
Returns true if no expected match failed, false otherwise.
## Examples
iex> Runner.run(Day0, "day0/input.example.txt")
"""
@spec run(module, String.t(), boolean) :: boolean
def run(daymodule, file, verbose \\ true) do
input = read_lines(file)
expected = read_expected(file)
len = Enum.count(input)
outcomes =
for part <- [:part1, :part2] do
if verbose, do: IO.puts(:stderr, "Running #{daymodule} #{part} on #{file} (#{len} lines)")
res = run_part(daymodule, part, input, Map.get(expected, part, ""))
try do
IO.puts("#{part}: #{res.got}")
rescue
Protocol.UndefinedError -> IO.inspect(res, label: part)
end
if verbose do
IO.puts(:stderr, Result.message(res))
IO.puts(:stderr, "#{part} took #{format_usec(res.time_usec)} on #{file}")
IO.puts(:stderr, String.duplicate("=", 40))
end
Result.ok?(res)
end
Enum.all?(outcomes)
end
@doc """
Runs function part of daymodule with the given iput and returns a
Runner.Result based on the function's output and whether it matches want.
## Examples
iex> Runner.run_part(Day0, :part1, ["123", "foo"], "42")
"""
@spec run_part(module, atom, [String.t()], String.t()) :: Result.t()
def run_part(daymodule, part, input, want) do
{usec, got} = :timer.tc(daymodule, part, [input])
outcome =
cond do
to_string(got) == to_string(want) -> :success
got == :todo -> :todo
want == "" -> :unknown
true -> :fail
end
%Result{outcome: outcome, got: got, want: want, time_usec: usec}
end
@doc """
Reads a file and splits it into a list of lines without trailing line breaks.
If file is "-" reads from stdio.
"""
@spec read_lines(String.t()) :: [String.t()]
def read_lines(file) do
case file do
"" -> raise ArgumentError, "empty filename"
"-" -> IO.stream()
_ -> File.stream!(file)
end
|> Stream.map(&String.trim_trailing(&1, "\n"))
|> Enum.to_list()
end
def read_expected(input_file) do
file = Path.rootname(input_file, ".txt") <> ".expected"
if File.exists?(file) do
for line <- read_lines(file), String.starts_with?(line, "part") do
[part, value] = String.split(line, ~r/:\s*/, parts: 2)
value = String.replace(value, "\\n", "\n", global: true)
{String.to_atom(part), value}
end
|> Map.new()
else
%{}
end
end
defp format_usec(0), do: "0μs"
defp format_usec(usec) when usec < 1000, do: "#{usec}μs"
defp format_usec(usec) when usec < 1_000_000, do: "#{usec / 1000}ms"
defp format_usec(usec) when usec < 60_000_000, do: "#{Float.round(usec / 1_000_000, 3)}s"
defp format_usec(usec) do
mins = Integer.to_string(rem(div(usec, 1_000_000), 60)) |> String.pad_leading(2, ["0"])
"#{div(usec, 60_000_000)}:#{mins}"
end
end