defmodule GitGud.Workflows.MatrixTest do
@moduledoc """
Matrix expansion, checked against the examples in GitHub's own
workflow-syntax documentation — `include` and `exclude` are the parts
people actually get wrong, and "it seemed reasonable" is not a
specification.
"""
use ExUnit.Case, async: true
alias GitGud.Workflows.Matrix
defp strategy(matrix), do: %{"matrix" => matrix}
describe "expand/1" do
test "no strategy is one leg with nothing in it" do
assert Matrix.expand(nil) == [%{}]
assert Matrix.expand(%{}) == [%{}]
assert Matrix.expand(%{"fail-fast" => false}) == [%{}]
end
test "one axis is one job per value" do
assert Matrix.expand(strategy(%{"arch" => ["amd64", "arm64"]})) == [
%{"arch" => "amd64"},
%{"arch" => "arm64"}
]
end
test "two axes are their product" do
combinations =
Matrix.expand(strategy(%{"arch" => ["amd64", "arm64"], "otp" => ["27", "28"]}))
assert length(combinations) == 4
assert %{"arch" => "amd64", "otp" => "27"} in combinations
assert %{"arch" => "arm64", "otp" => "28"} in combinations
end
test "a scalar value is a single-value axis" do
assert Matrix.expand(strategy(%{"arch" => "arm64"})) == [%{"arch" => "arm64"}]
end
test "expansion is stable — map order must not decide job order" do
matrix = %{"arch" => ["amd64", "arm64"], "otp" => ["27", "28"], "os" => ["a", "b"]}
assert Matrix.expand(strategy(matrix)) == Matrix.expand(strategy(matrix))
end
end
describe "exclude" do
test "drops combinations matching every key it names" do
combinations =
Matrix.expand(
strategy(%{
"arch" => ["amd64", "arm64"],
"otp" => ["27", "28"],
"exclude" => [%{"arch" => "arm64", "otp" => "27"}]
})
)
assert length(combinations) == 3
refute %{"arch" => "arm64", "otp" => "27"} in combinations
end
test "a partial exclude drops every combination it matches" do
combinations =
Matrix.expand(
strategy(%{
"arch" => ["amd64", "arm64"],
"otp" => ["27", "28"],
"exclude" => [%{"arch" => "arm64"}]
})
)
assert combinations == [
%{"arch" => "amd64", "otp" => "27"},
%{"arch" => "amd64", "otp" => "28"}
]
end
end
describe "include" do
test "adds keys to the combinations it matches rather than appending a job" do
combinations =
Matrix.expand(
strategy(%{
"arch" => ["amd64", "arm64"],
"include" => [%{"arch" => "arm64", "coverage" => true}]
})
)
# Still two jobs — the arm64 one just gained a key.
assert combinations == [
%{"arch" => "amd64"},
%{"arch" => "arm64", "coverage" => true}
]
end
test "an entry matching nothing becomes a combination of its own" do
combinations =
Matrix.expand(
strategy(%{
"arch" => ["amd64", "arm64"],
"include" => [%{"arch" => "riscv64"}]
})
)
assert combinations == [
%{"arch" => "amd64"},
%{"arch" => "arm64"},
%{"arch" => "riscv64"}
]
end
test "an entry with no overlapping keys applies to every combination" do
combinations =
Matrix.expand(
strategy(%{
"arch" => ["amd64", "arm64"],
"include" => [%{"otp" => "28"}]
})
)
assert combinations == [
%{"arch" => "amd64", "otp" => "28"},
%{"arch" => "arm64", "otp" => "28"}
]
end
test "include runs after exclude, so it can add a combination back" do
combinations =
Matrix.expand(
strategy(%{
"arch" => ["amd64", "arm64"],
"exclude" => [%{"arch" => "arm64"}],
"include" => [%{"arch" => "arm64", "experimental" => true}]
})
)
assert combinations == [
%{"arch" => "amd64"},
%{"arch" => "arm64", "experimental" => true}
]
end
test "an include-only matrix is just its entries" do
combinations =
Matrix.expand(
strategy(%{
"include" => [
%{"arch" => "amd64", "runner" => "big"},
%{"arch" => "arm64", "runner" => "small"}
]
})
)
assert combinations == [
%{"arch" => "amd64", "runner" => "big"},
%{"arch" => "arm64", "runner" => "small"}
]
end
end
describe "substitute/2" do
test "resolves matrix references in a runs-on" do
assert Matrix.substitute("${{ matrix.arch }}", %{"arch" => "arm64"}) == "arm64"
assert Matrix.substitute("${{matrix.arch}}", %{"arch" => "arm64"}) == "arm64"
assert Matrix.substitute("linux-${{ matrix.arch }}", %{"arch" => "arm64"}) == "linux-arm64"
end
test "resolves inside a list, which is how an architecture is usually selected" do
assert Matrix.substitute(["self-hosted", "${{ matrix.arch }}"], %{"arch" => "arm64"}) ==
["self-hosted", "arm64"]
end
test "an unknown key resolves to empty, as it does in Actions" do
assert Matrix.substitute("${{ matrix.nope }}", %{"arch" => "arm64"}) == ""
end
test "leaves other expressions alone — the runner evaluates those" do
assert Matrix.substitute("${{ github.sha }}", %{"arch" => "arm64"}) == "${{ github.sha }}"
end
test "non-strings pass through" do
assert Matrix.substitute(42, %{}) == 42
assert Matrix.substitute(nil, %{}) == nil
end
end
describe "labels/1" do
test "a scalar and a one-element list are the same thing" do
assert Matrix.labels("ubuntu-latest") == ["ubuntu-latest"]
assert Matrix.labels(["ubuntu-latest"]) == ["ubuntu-latest"]
end
test "keeps every label in a list — all of them must be satisfied" do
assert Matrix.labels(["self-hosted", "arm64"]) == ["self-hosted", "arm64"]
end
test "drops blanks and duplicates" do
assert Matrix.labels(["self-hosted", "", "self-hosted"]) == ["self-hosted"]
end
end
describe "pin_definition/2" do
test "pins the matrix to this leg so the runner expands it to exactly one job" do
definition = %{
"runs-on" => "self-hosted",
"strategy" => %{"matrix" => %{"arch" => ["amd64", "arm64"]}, "fail-fast" => false},
"steps" => [%{"run" => "echo ${{ matrix.arch }}"}]
}
pinned = Matrix.pin_definition(definition, %{"arch" => "arm64"})
# One value, so act fans out to one job and the `matrix` context
# still resolves for the step.
# `fail-fast` rides along: the forge reads it back off the stored
# definition, and this is the only copy that survives.
assert pinned["strategy"] == %{"matrix" => %{"arch" => ["arm64"]}, "fail-fast" => false}
assert pinned["steps"] == definition["steps"]
end
test "a job with no matrix loses its strategy entirely" do
definition = %{"strategy" => %{"fail-fast" => false}, "runs-on" => "x"}
assert Matrix.pin_definition(definition, %{}) == %{"runs-on" => "x"}
end
end
describe "suffix/1 and fail_fast?/1" do
test "names a leg the way Actions does" do
assert Matrix.suffix(%{}) == nil
assert Matrix.suffix(%{"arch" => "arm64"}) == "arm64"
assert Matrix.suffix(%{"arch" => "arm64", "otp" => "28"}) == "arm64, 28"
end
test "fail-fast defaults on, and only an explicit false turns it off" do
assert Matrix.fail_fast?(nil)
assert Matrix.fail_fast?(%{"matrix" => %{}})
refute Matrix.fail_fast?(%{"fail-fast" => false})
refute Matrix.fail_fast?(%{"fail-fast" => "false"})
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
7.8 KiB · text
History
cac71de