Skip to content
This repository was archived by the owner on Jun 10, 2024. It is now read-only.

Commit 9e862a3

Browse files
committed
mix lumen.otp.log.parse
`mix lumen.otp.log.parse` can parse the output of `lumen::otp` tests and classify the errors into a CSV. (Actually tab-separated as some error messages contain commas.)
1 parent ca9088a commit 9e862a3

File tree

8 files changed

+360
-0
lines changed

8 files changed

+360
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
lumen_otp_log_parser-*.tar
24+
25+
26+
# Temporary files for e.g. tests
27+
/tmp
28+
29+
/test.log.csv
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Lumen.OTP.Log.Parser
2+
3+
Parses the log from `cargo test`
4+
5+
1. `cd ../../../..`
6+
2. Run tests and put ANSI-stripped output to `test.log`: `cargo make test -- --package liblumen_otp lumen::otp:: 2&>1 | sed 's/\x1b\[[0-9;]*m//g' | tee test.log`
7+
1. `cd native_implemented/otp/tests/log_parser`
8+
3. Parse those log into CSV: `mix lumen.otp.log.parse ../../../../test.log > test.log.csv`
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
defmodule Lumen.OTP.Log.Parser do
2+
@moduledoc """
3+
Documentation for `Lumen.OTP.Log.Parser`.
4+
"""
5+
6+
def parse(path) do
7+
path
8+
|> File.stream!()
9+
# drop until the first `failures:\n` that is the start of the failure details
10+
|> Stream.drop_while(fn
11+
"failures:\n" -> false
12+
_ -> true
13+
end)
14+
|> Stream.drop(2)
15+
# take while the second `failures:\n` that is summary of test names is shown and details end
16+
|> Stream.take_while(fn
17+
"failures:\n" -> false
18+
_ -> true
19+
end)
20+
|> Stream.filter(fn
21+
# remove common status code
22+
"Status code: 101\n" -> false
23+
# remove common signal (none)
24+
"Signal: ," <> _ -> false
25+
_ -> true
26+
end)
27+
|> Stream.chunk_while(
28+
{nil, []},
29+
fn
30+
"---- lumen::otp::" <> suffix, {previous_test, previous_test_lines} ->
31+
test =
32+
suffix
33+
|> String.split(" ", parts: 2)
34+
|> hd()
35+
36+
{:cont, {previous_test, Enum.reverse(previous_test_lines)}, {test, []}}
37+
38+
line, {test, acc_lines} ->
39+
{:cont, {test, [line | acc_lines]}}
40+
end,
41+
fn {previous_test, previous_test_lines} ->
42+
{:cont, {previous_test, Enum.reverse(previous_test_lines)}, {nil, []}}
43+
end
44+
)
45+
|> Stream.drop(1)
46+
|> Stream.map(fn {test, lines} ->
47+
case classify(lines) do
48+
{:ok, classification} ->
49+
{test, classification}
50+
51+
:error ->
52+
Mix.shell().error("Could not classify #{test} error:\n#{Enum.join(lines)}")
53+
System.halt(1)
54+
end
55+
end)
56+
|> Enum.sort()
57+
|> Enum.each(fn {test, classification} ->
58+
Mix.shell().info("#{test}\t#{classification}")
59+
end)
60+
end
61+
62+
defp classify([]) do
63+
:error
64+
end
65+
66+
defp classify([" right: `SourceId" <> _ = line | tail]) do
67+
if String.contains?(line, "source spans cannot start and end in different files") do
68+
{:ok, "source spans cannot start and end in different files"}
69+
else
70+
classify(tail)
71+
end
72+
end
73+
74+
defp classify([~S|Assertion failed: (*this && "isa<> used on a null type.")| <> _ | _]) do
75+
{:ok, "isa<> used on a null type"}
76+
end
77+
78+
defp classify([
79+
~S|Assertion failed: (funcOp.isExternal() && "cannot define a function more than once"| <>
80+
_
81+
| _
82+
]) do
83+
{:ok, "cannot define a function more than once"}
84+
end
85+
86+
defp classify(["Assertion failed: (hasVal), function getValue, " <> suffix | tail]) do
87+
if String.contains?(suffix, "llvm/ADT/Optional.h") do
88+
{:ok, "optional does not have value"}
89+
else
90+
classify(tail)
91+
end
92+
end
93+
94+
defp classify(["error: attribute is already defined\n" | _]) do
95+
{:ok, "attribute is already defined"}
96+
end
97+
98+
defp classify(["error: could not find file\n" | _]) do
99+
{:ok, "could not find file"}
100+
end
101+
102+
defp classify(["error: could not resolve variable\n" | _]) do
103+
{:ok, "could not resolve variable"}
104+
end
105+
106+
defp classify(["error: 'eir.binary.match.integer' op attribute 'unit' failed to satisfy constraint: 64-bit signless integer attribute\n" | _]) do
107+
{:ok, "'eir.binary.match.integer' op attribute 'unit' failed to satisfy constraint: 64-bit signless integer attribute"}
108+
end
109+
110+
defp classify([
111+
"error: 'eir.logical.and' op result #0 must be 1-bit signless integer, but got '!eir.bool'\n"
112+
| _
113+
]) do
114+
{:ok, "'eir.logical.and' op result must be 1-bit signless integer, but got '!eir.bool'"}
115+
end
116+
117+
defp classify([
118+
"error: 'eir.logical.or' op result #0 must be 1-bit signless integer, but got '!eir.bool'\n"
119+
| _
120+
]) do
121+
{:ok, "'eir.logical.or' op result must be 1-bit signless integer, but got '!eir.bool'"}
122+
end
123+
124+
defp classify([
125+
"error: 'eir.map.contains' op operand #1 must be dynamic term type, but got '!eir.atom'\n"
126+
| _
127+
]) do
128+
{:ok, "'eir.map.contains' op operand #1 must be dynamic term type"}
129+
end
130+
131+
defp classify([
132+
<<"error: 'eir.map.insert' op operand #", _, " must be dynamic term type">> <> _ | _
133+
]) do
134+
{:ok, "'eir.map.insert' op operand #0 must be dynamic term type"}
135+
end
136+
137+
defp classify(["error: 'eir.map.update' op operand #0 must be dynamic term type, but got '!eir.box<!eir.map>'\n" | _]) do
138+
{:ok, "'eir.map.update' op operand #0 must be dynamic term type"}
139+
end
140+
141+
defp classify([
142+
"error: 'eir.throw' op operand #2 must be opaque trace reference, but got '!eir.term'\n"
143+
| _
144+
]) do
145+
{:ok, "'eir.throw' op operand #2 must be opaque trace reference, but got '!eir.term'"}
146+
end
147+
148+
defp classify(["error: invalid cast type, source type is unsupported\n" | _]) do
149+
{:ok, "invalid cast type, source type is unsupported"}
150+
end
151+
152+
defp classify(["error: invalid const expression\n" | _]) do
153+
{:ok, "invalid const expression"}
154+
end
155+
156+
defp classify(["error: invalid string escape\n" | _]) do
157+
{:ok, "invalid string escape"}
158+
end
159+
160+
defp classify(["error: invalid tuple type element" <> _ | _]) do
161+
{:ok, "invalid tuple type element"}
162+
end
163+
164+
defp classify([<<"error: operand #", _, " does not dominate this use">> <> _ | _]) do
165+
{:ok, "operand does not dominate this use"}
166+
end
167+
168+
defp classify(["error: operand type '!eir.nil' and result type '!eir.box<!eir.cons>' are not cast compatible\n" | _]) do
169+
{:ok, "operand type '!eir.nil' and result type '!eir.box<!eir.cons>' are not cast compatible"}
170+
end
171+
172+
defp classify(["error: redefinition of symbol" <> _ | _]) do
173+
{:ok, "redefinition of symbol"}
174+
end
175+
176+
defp classify(["error: undefined macro\n" | _]) do
177+
{:ok, "undefined macro"}
178+
end
179+
180+
defp classify(["error: unrecognized token\n" | _]) do
181+
{:ok, "unrecognized token"}
182+
end
183+
184+
defp classify(["stderr: error: invalid input file" <> _ | _]) do
185+
{:ok, "invalid input file"}
186+
end
187+
188+
defp classify(["thread '<unknown>' has overflowed its stack\n" | _]) do
189+
{:ok, "stack overflow"}
190+
end
191+
192+
defp classify(["thread '<unnamed>' panicked at 'expected constant const_value" <> suffix | tail]) do
193+
[_, after_number] = String.split(suffix, " ", parts: 2)
194+
195+
case after_number do
196+
"to be an atom'" <> _ -> {:ok, "expected constant to be an atom"}
197+
_ -> classify(tail)
198+
end
199+
end
200+
201+
defp classify(["thread '<unnamed>' panicked at 'expected primop value'" <> _ | _]) do
202+
{:ok, "expected primop value"}
203+
end
204+
205+
defp classify(["thread '<unnamed>' panicked at 'expected value, but got pseudo-value'" <> _ | _]) do
206+
{:ok, "expected value, but go pseudo-value"}
207+
end
208+
209+
defp classify([
210+
"thread '<unnamed>' panicked at 'invalid operation kind: UnpackValueList" <> _ | _
211+
]) do
212+
{:ok, "invalid operation kind: UnpackValueList"}
213+
end
214+
215+
defp classify(["thread '<unnamed>' panicked at 'no entry found for key', " <> suffix | tail]) do
216+
if String.ends_with?(suffix, "libeir_syntax_erl/src/lower/expr/record.rs:138:19\n") do
217+
{:ok, "no entry found for key in record"}
218+
else
219+
classify(tail)
220+
end
221+
end
222+
223+
defp classify(["thread '<unnamed>' panicked at 'not implemented', " <> suffix | tail]) do
224+
if String.ends_with?(suffix, "libeir_syntax_erl/src/lower/expr/comprehension.rs:130:44\n") do
225+
{:ok, "binary generators not implemented"}
226+
else
227+
classify(tail)
228+
end
229+
end
230+
231+
defp classify([
232+
"thread '<unnamed>' panicked at 'not yet implemented: unimplemented call type LocalDynamic" <>
233+
_
234+
| _
235+
]) do
236+
{:ok, "unimplemented call type LocalDynamic"}
237+
end
238+
239+
defp classify(["thread '<unnamed>' panicked at 'the given value is not a known block" <> _ | _]) do
240+
{:ok, "the given value is not a known block"}
241+
end
242+
243+
defp classify(["thread 'lumen::otp::" <> suffix | tail]) do
244+
if String.contains?(suffix, "Compilation timed out") do
245+
{:ok, "compilation timed out"}
246+
else
247+
classify(tail)
248+
end
249+
end
250+
251+
defp classify(["Undefined symbols" <> _ | _]) do
252+
{:ok, "undefined symbols"}
253+
end
254+
255+
defp classify(["warning: invalid compile option\n" | _]) do
256+
{:ok, "invalid compile option"}
257+
end
258+
259+
defp classify(["stdout: TODO file directive -file" <> _ | _]) do
260+
{:ok, "file directive (-file) unimplemented"}
261+
end
262+
263+
defp classify([_ | tail]), do: classify(tail)
264+
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
defmodule Mix.Tasks.Lumen.Otp.Log.Parse do
2+
@moduledoc "Parses the test.log outputted from `cargo test --package liblumen_otp -- lumen::otp`"
3+
@shortdoc "Parses test.log"
4+
5+
alias Lumen.OTP.Log.Parser
6+
7+
use Mix.Task
8+
9+
@impl Mix.Task
10+
11+
def run([path]) do
12+
Parser.parse(path)
13+
end
14+
15+
def run(_) do
16+
Mix.shell().info("mix lumen.otp.log.parse LOG_PATH")
17+
end
18+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule Lumen.OTP.Log.Parser.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :lumen_otp_log_parser,
7+
version: "0.1.0",
8+
elixir: "~> 1.11",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
# Run "mix help compile.app" to learn about applications.
15+
def application do
16+
[
17+
extra_applications: [:logger]
18+
]
19+
end
20+
21+
# Run "mix help deps" to learn about dependencies.
22+
defp deps do
23+
[
24+
# {:dep_from_hexpm, "~> 0.3.0"},
25+
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
26+
]
27+
end
28+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule Lumen.OTP.Log.ParserTest do
2+
use ExUnit.Case
3+
doctest Lumen.OTP.Log.Parser
4+
5+
test "greets the world" do
6+
assert Lumen.OTP.Log.Parser.hello() == :world
7+
end
8+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ExUnit.start()

0 commit comments

Comments
 (0)