Skip to content

Commit a351944

Browse files
committed
Add e2e_data.exs, driving local e2e tests
1 parent 0a0893e commit a351944

File tree

4 files changed

+194
-12
lines changed

4 files changed

+194
-12
lines changed

lib/e2e.ex

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
defmodule ElixirScript.E2e.Entry do
2+
@moduledoc """
3+
A struct representing the data for an E2E test
4+
"""
5+
6+
defstruct id: nil, name: nil, script: nil, file: nil, expected: nil
7+
end
8+
9+
defmodule ElixirScript.E2e do
10+
alias ElixirScript.E2e.Entry
11+
12+
def read_test_file(file_path \\ "test/e2e_data.exs") do
13+
{data, _} = file_path
14+
|> Code.eval_file()
15+
16+
data
17+
|> Enum.map(&process_entry/1)
18+
end
19+
20+
defp process_entry(entry) do
21+
name = Map.fetch!(entry, :name)
22+
script = Map.get(entry, :script)
23+
file = Map.get(entry, :file)
24+
expected = Map.get(entry, :expected)
25+
26+
if(!script && !file) do
27+
raise(KeyError, "key :script or :file not found in: #{inspect(entry)}")
28+
end
29+
30+
%Entry{
31+
id: slugify(name),
32+
name: name,
33+
script: script,
34+
file: file,
35+
expected: expected
36+
}
37+
end
38+
39+
defp slugify(name), do: String.downcase(String.replace(name, ~r/\s+/, "-"))
40+
end

test/e2e_data.exs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[
2+
%{
3+
name: "Scripts are run, and what it returns is available via outputs",
4+
script: """
5+
defmodule Foo do
6+
def bar, do: "bar"
7+
end
8+
Foo.bar()
9+
""",
10+
expected: "bar"
11+
},
12+
%{
13+
name: "IO is visible in logs",
14+
script: "IO.puts(\"Hello world\")"
15+
},
16+
%{
17+
name: "Event context is available",
18+
script: "Map.keys(context) |> Enum.sort",
19+
expected: "[__struct__,action,actor,api_url,event_name,graphql_url,job,payload,ref,run_id,run_number,server_url,sha,workflow]"
20+
},
21+
]

test/e2e_test.exs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
defmodule ElixirScript.E2eTest do
2+
@moduledoc """
3+
This module contains unit tests for the E2e module's functionality,
4+
ensuring that the test file reading and parsing behaviors are working as expected.
5+
"""
6+
use ExUnit.Case
7+
8+
alias ElixirScript.E2e
9+
alias ElixirScript.E2eTest.Runner
10+
11+
describe "read_test_file" do
12+
test "when providing a name it gets slugified" do
13+
file_path = create_temp_file([ %{ name: "Test name", script: "" } ])
14+
result = E2e.read_test_file(file_path) |> List.first() |> Map.take([:name, :id])
15+
assert result == %{ name: "Test name", id: "test-name" }
16+
end
17+
18+
test "can read a simple example script" do
19+
file_path = create_temp_file([ %{ name: "Test name", script: "IO.puts(\"Hello, world!\")" } ])
20+
result = E2e.read_test_file(file_path) |> List.first() |> Map.take([:file, :script])
21+
assert result == %{ file: nil, script: "IO.puts(\"Hello, world!\")", }
22+
end
23+
24+
test "can read an expected value" do
25+
file_path = create_temp_file([ %{ name: "Test name", script: "\"foo\"", expected: "foo" } ])
26+
result = E2e.read_test_file(file_path) |> List.first() |> Map.take([:expected])
27+
assert result == %{ expected: "foo" }
28+
end
29+
30+
test "can read a file example" do
31+
file_path = create_temp_file([ %{ name: "Test name", file: "./foo.ex" } ])
32+
result = E2e.read_test_file(file_path) |> List.first() |> Map.take([:file, :script])
33+
assert result == %{ file: "./foo.ex", script: nil, }
34+
end
35+
36+
test "fails if neither script nor file is specified" do
37+
file_path = create_temp_file([ %{ name: "Test name" } ])
38+
assert_raise KeyError, "key :script or :file not found in: %{name: \"Test name\"}", fn ->
39+
E2e.read_test_file(file_path)
40+
end
41+
end
42+
43+
test "fails if name is not specified" do
44+
file_path = create_temp_file([ %{ script: "" } ])
45+
assert_raise KeyError, "key :name not found in: %{script: \"\"}", fn ->
46+
E2e.read_test_file(file_path)
47+
end
48+
end
49+
end
50+
51+
describe "end-to-end tests" do
52+
test "run e2e tests" do
53+
Enum.each(E2e.read_test_file(), &Runner.run_test/1)
54+
end
55+
end
56+
57+
defp create_temp_file(content) do
58+
tmp_dir = System.tmp_dir()
59+
file_path = Path.join(tmp_dir, "temp.exs")
60+
File.write!(file_path, inspect(content))
61+
on_exit(fn -> File.rm!(file_path) end)
62+
file_path
63+
end
64+
end
65+
66+
defmodule ElixirScript.E2eTest.Runner do
67+
@moduledoc """
68+
This submodule focuses on running end-to-end tests and validating their output against expected results.
69+
"""
70+
use ExUnit.Case
71+
72+
import ExUnit.CaptureIO
73+
74+
alias ElixirScript.E2e.Entry
75+
alias ElixirScript.ScriptRunner
76+
77+
def run_test(%Entry{name: name, file: nil, script: script, expected: expected}) do
78+
actual = run_script_and_capture_output(script)
79+
|> convert_to_github_actions_output()
80+
81+
unless is_nil(expected) do
82+
assert actual == expected,
83+
"E2E test '#{name}' failed.\n Expected: #{inspect(expected)}\n Actual: #{inspect(actual)}"
84+
end
85+
end
86+
87+
# Runs the script and captures the return value by isolating it from other IO outputs.
88+
#
89+
# `ScriptRunner.run/1` executes a script that may produce additional output, such as logs.
90+
# To capture only the return value of the script, we spawn a new process that sends
91+
# the return value back to the parent process. This ensures that only the intended return
92+
# value is used for the test assertion, regardless of any other IO produced during script execution.
93+
defp run_script_and_capture_output(script) do
94+
capture_io(fn ->
95+
actual = ScriptRunner.run(script)
96+
send(self(), {:actual, actual})
97+
end)
98+
99+
assert_received {:actual, actual}
100+
actual
101+
end
102+
103+
# Converts the script output to match the GitHub Actions output format.
104+
#
105+
# In GitHub Actions, complex Elixir data structures are not automatically handled.
106+
# Instead, they are converted to strings, because they are transported via environment variables.
107+
#
108+
# This function takes the output and converts it into a string format that mirrors
109+
# the stringification behavior seen in GitHub Actions.
110+
#
111+
# Examples:
112+
# - An Elixir list of atoms like `[:foo, :bar]` would be converted to `"[foo,bar]"`.
113+
#
114+
# @return A string that matches the expected output format of GitHub Actions.
115+
defp convert_to_github_actions_output(data) do
116+
case data do
117+
binary when is_binary(binary) -> binary # Already a string, no change needed.
118+
list when is_list(list) -> # Serialize the list as GitHub Actions would output it.
119+
list
120+
|> Enum.map(&convert_element_to_string/1)
121+
|> Enum.join(",")
122+
|> wrap_in_brackets()
123+
_ ->
124+
inspect(data)
125+
|> String.trim_leading("\"")
126+
|> String.trim_trailing("\"")
127+
end
128+
end
129+
130+
defp convert_element_to_string(element) when is_atom(element), do: Atom.to_string(element)
131+
defp convert_element_to_string(element), do: element # For any non-atom elements.
132+
defp wrap_in_brackets(joined), do: "[#{joined}]"
133+
end

test/e2e_tests.json

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)