# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Tests for `repomatic.matrix.model`: the workflow matrix model."""
from __future__ import annotations
from collections import Counter
from itertools import permutations
import pytest
from repomatic.matrix.model import GITHUB_MATRIX_JOB_LIMIT, Matrix
[docs]
def test_matrix_constructor_takes_no_arguments():
"""A matrix is populated through its methods, never through the constructor.
The signature used to swallow `*args, **kwargs`, so `Matrix({"os": [...]})`
returned an empty matrix instead of raising.
"""
with pytest.raises(TypeError):
Matrix({"os": ["ubuntu-slim"]}) # type: ignore[call-arg]
[docs]
def test_matrix():
"""Construction, deduplicating `add_variation`, rendering, and rejections.
`include` and `exclude` are reserved: they name the directive lists, so
accepting them as axis names would shadow the very keys `solve` reads.
"""
matrix = Matrix()
assert isinstance(matrix, Matrix)
assert not isinstance(matrix, dict)
assert hasattr(matrix, "variations")
assert isinstance(matrix.variations, dict)
assert hasattr(matrix, "include")
assert hasattr(matrix, "exclude")
assert matrix.include == ()
assert matrix.exclude == ()
matrix.add_variation("foo", ["a", "b", "c"])
assert matrix.variations == {"foo": ("a", "b", "c")}
assert not matrix.include
assert not matrix.exclude
# Natural deduplication.
matrix.add_variation("foo", ["a", "a", "d"])
assert matrix.variations == {"foo": ("a", "b", "c", "d")}
assert not matrix.include
assert not matrix.exclude
assert matrix.matrix() == {"foo": ("a", "b", "c", "d")}
assert str(matrix) == '{"foo": ["a", "b", "c", "d"]}'
assert repr(matrix) == "<Matrix: {'foo': ('a', 'b', 'c', 'd')}>"
with pytest.raises(ValueError):
matrix.add_variation("variation_1", None) # type: ignore[arg-type]
with pytest.raises(ValueError):
matrix.add_variation("variation_1", [])
with pytest.raises(ValueError):
matrix.add_variation("variation_1", [None]) # type: ignore[list-item]
with pytest.raises(ValueError):
matrix.add_variation("include", ["a", "b", "c"])
with pytest.raises(ValueError):
matrix.add_variation("exclude", ["a", "b", "c"])
[docs]
def test_add_variation_reads_a_generator_once():
"""A generator of values lands whole, rather than consumed by the checks."""
matrix = Matrix()
matrix.add_variation("fruit", (fruit for fruit in ("apple", "pear", "apple")))
assert matrix.variations == {"fruit": ("apple", "pear")}
[docs]
def test_replace_variation_value():
"""`replace` swaps one axis value for another, in place."""
matrix = Matrix()
matrix.add_variation("os", ["ubuntu-slim", "macos-26", "windows-2025"])
matrix.add_variation("version", ["3.10", "3.14"])
# Basic replacement preserves position.
matrix.replace_variation_value("os", "ubuntu-slim", "ubuntu-24.04")
assert matrix.variations["os"] == ("ubuntu-24.04", "macos-26", "windows-2025")
# Other axes are untouched.
assert matrix.variations["version"] == ("3.10", "3.14")
# Missing axis is a silent no-op.
matrix.replace_variation_value("nonexistent", "a", "b")
assert "nonexistent" not in matrix.variations
# Missing value within an existing axis is a silent no-op.
matrix.replace_variation_value("os", "linux-latest", "ubuntu-22.04")
assert matrix.variations["os"] == ("ubuntu-24.04", "macos-26", "windows-2025")
# Replacement deduplicates when the new value already exists.
matrix.replace_variation_value("os", "windows-2025", "macos-26")
assert matrix.variations["os"] == ("ubuntu-24.04", "macos-26")
[docs]
def test_remove_variation_value():
"""`remove` drops an axis value, and the axis once it empties."""
matrix = Matrix()
matrix.add_variation("os", ["ubuntu-slim", "macos-26", "windows-2025"])
matrix.add_variation("version", ["3.10", "3.14"])
# Basic removal.
matrix.remove_variation_value("os", "ubuntu-slim")
assert matrix.variations["os"] == ("macos-26", "windows-2025")
# Other axes are untouched.
assert matrix.variations["version"] == ("3.10", "3.14")
# Missing axis is a silent no-op.
matrix.remove_variation_value("nonexistent", "a")
assert "nonexistent" not in matrix.variations
# Missing value within an existing axis is a silent no-op.
matrix.remove_variation_value("os", "linux-latest")
assert matrix.variations["os"] == ("macos-26", "windows-2025")
# Removing all values deletes the axis.
matrix.remove_variation_value("os", "macos-26")
matrix.remove_variation_value("os", "windows-2025")
assert "os" not in matrix.variations
[docs]
def test_prune(caplog):
"""Prune removes no-op excludes and logs about them."""
matrix = Matrix()
matrix.add_variation("os", ["ubuntu-24.04", "macos-26"])
matrix.add_variation("version", ["3.10", "3.14"])
# Effective exclude: os value exists in the axis.
matrix.add_excludes({"os": "macos-26", "version": "3.10"})
# No-op exclude: os value does not exist in the axis.
matrix.add_excludes({"os": "windows-11-arm"})
# No-op exclude: version value does not exist in the axis.
matrix.add_excludes({"version": "3.15t"})
# No-op exclude: key not in any axis. GitHub Actions rejects excludes
# referencing non-existent matrix keys, so prune must drop these too.
matrix.add_excludes({"state": "unstable"})
assert len(matrix.exclude) == 4
matrix.prune()
# Only the effective exclude remains. Reassign with an explicit
# annotation to widen the type back to a variable-length tuple. The
# ``assert len(...) == 4`` above narrows ``matrix.exclude`` to a
# fixed-length 4-tuple; mypy cannot track the mutation performed by
# ``prune()``, so without this it considers the ``len(...) == 1``
# assert unreachable.
exclude: tuple[dict[str, str], ...] = matrix.exclude
assert len(exclude) == 1
assert {"os": "macos-26", "version": "3.10"} in exclude
assert "Dropping no-op exclude" in caplog.text
assert "'windows-11-arm'" in caplog.text
assert "'3.15t'" in caplog.text
assert "'state'" in caplog.text
[docs]
def test_remove_variation_value_solve():
"""Exclude prevents resurrection; removal also strips the axis value."""
matrix = Matrix()
matrix.add_variation("os", ["ubuntu-slim", "macos-26", "windows-11-arm"])
matrix.add_variation("version", ["3.14", "3.15"])
matrix.add_includes({"state": "stable"})
matrix.add_includes({"state": "unstable", "version": "3.15"})
matrix.add_excludes({"os": "windows-11-arm"})
# The partial unstable include augments the surviving 3.15 jobs; it does not
# resurrect the excluded windows-11-arm × 3.15 combination (GitHub's
# behavior). windows-11-arm stays a declared axis value, only its
# combinations are excluded.
solved_with_exclude = tuple(matrix.solve())
assert all(j["os"] != "windows-11-arm" for j in solved_with_exclude)
assert len(solved_with_exclude) == 4
assert {"os": "macos-26", "version": "3.15", "state": "unstable"} in (
solved_with_exclude
)
assert "windows-11-arm" in matrix.variations["os"]
# Removal additionally strips the value from the axis entirely.
matrix.remove_variation_value("os", "windows-11-arm")
solved_with_remove = tuple(matrix.solve())
assert all(j["os"] != "windows-11-arm" for j in solved_with_remove)
assert len(solved_with_remove) == 4
assert "windows-11-arm" not in matrix.variations["os"]
[docs]
def test_solve_matches_github_include_example():
"""solve() reproduces GitHub's documented matrix include expansion.
The canonical example from [GitHub's
docs](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/run-job-variations#example-expanding-configurations):
later includes augment base combinations only, never combinations a previous
include created, and an include that matches no combination is appended.
"""
matrix = Matrix()
matrix.add_variation("fruit", ["apple", "pear"])
matrix.add_variation("animal", ["cat", "dog"])
matrix.add_includes({"color": "green"})
matrix.add_includes({"color": "pink", "animal": "cat"})
matrix.add_includes({"fruit": "apple", "shape": "circle"})
matrix.add_includes({"fruit": "banana"})
matrix.add_includes({"fruit": "banana", "animal": "cat"})
assert tuple(matrix.solve()) == (
{"fruit": "apple", "animal": "cat", "color": "pink", "shape": "circle"},
{"fruit": "apple", "animal": "dog", "color": "green", "shape": "circle"},
{"fruit": "pear", "animal": "cat", "color": "pink"},
{"fruit": "pear", "animal": "dog", "color": "green"},
{"fruit": "banana"},
{"fruit": "banana", "animal": "cat"},
)
[docs]
def test_solve_partial_include_does_not_resurrect_excluded():
"""A partial include augments survivors without resurrecting excluded combos."""
matrix = Matrix()
matrix.add_variation("os", ["ubuntu-slim", "macos-26", "windows-2025"])
matrix.add_variation("python-version", ["3.10", "3.15"])
# Carve the 3.15 probe down to a single runner.
matrix.add_excludes(
{"os": "macos-26", "python-version": "3.15"},
{"os": "windows-2025", "python-version": "3.15"},
)
# A partial include flags every surviving 3.15 job unstable.
matrix.add_includes({"state": "unstable", "python-version": "3.15"})
solved = tuple(matrix.solve())
# Only ubuntu-slim keeps a 3.15 job; the excluded ones stay gone.
py315 = [j for j in solved if j["python-version"] == "3.15"]
assert py315 == [
{"os": "ubuntu-slim", "python-version": "3.15", "state": "unstable"}
]
assert len(solved) == 4
[docs]
def test_solve_full_include_resurrects_excluded():
"""A fully-specified include adds back an excluded combination (GitHub behavior)."""
matrix = Matrix()
matrix.add_variation("os", ["ubuntu-slim", "macos-26"])
matrix.add_variation("python-version", ["3.10", "3.14"])
matrix.add_excludes({"os": "macos-26", "python-version": "3.14"})
# An include that re-specifies the excluded cell brings it back.
matrix.add_includes({"os": "macos-26", "python-version": "3.14", "extra": "yes"})
solved = tuple(matrix.solve())
assert {"os": "macos-26", "python-version": "3.14", "extra": "yes"} in solved
assert len(solved) == 4
[docs]
def test_replace_variation_value_solve():
"""Replacement integrates correctly with the full solve pipeline."""
matrix = Matrix()
matrix.add_variation("os", ["ubuntu-slim", "macos-26"])
matrix.add_variation("version", ["3.10", "3.14"])
matrix.add_includes({"state": "stable"})
matrix.add_excludes({"os": "ubuntu-slim", "version": "3.10"})
matrix.replace_variation_value("os", "ubuntu-slim", "ubuntu-24.04")
# The exclude still references "ubuntu-slim", which no longer exists in the
# axis, so the combination is no longer excluded.
assert tuple(matrix.solve()) == (
{"os": "ubuntu-24.04", "version": "3.10", "state": "stable"},
{"os": "ubuntu-24.04", "version": "3.14", "state": "stable"},
{"os": "macos-26", "version": "3.10", "state": "stable"},
{"os": "macos-26", "version": "3.14", "state": "stable"},
)
[docs]
@pytest.mark.parametrize("directive", ["include", "exclude"])
def test_include_and_exclude_directives(directive):
"""`add_includes` and `add_excludes` accumulate without touching the axes."""
matrix = Matrix()
add = getattr(matrix, f"add_{directive}s")
matrix.add_variation("foo", ["a", "b", "c"])
assert matrix.matrix() == {"foo": ("a", "b", "c")}
# First addition.
add({"foo": "a", "bar": "1"})
assert matrix.matrix() == {
"foo": ("a", "b", "c"),
directive: ({"foo": "a", "bar": "1"},),
}
# Second addition is cumulative.
add({"foo": "b", "bar": "2"})
assert matrix.matrix() == {
"foo": ("a", "b", "c"),
directive: ({"foo": "a", "bar": "1"}, {"foo": "b", "bar": "2"}),
}
# Deduplication.
add({"foo": "b", "bar": "2"})
assert matrix.matrix() == {
"foo": ("a", "b", "c"),
directive: ({"foo": "a", "bar": "1"}, {"foo": "b", "bar": "2"}),
}
# Rendering.
assert str(matrix) == (
'{"foo": ["a", "b", "c"], '
'"DIRECTIVE": [{"foo": "a", "bar": "1"}, {"foo": "b", "bar": "2"}]}'
).replace("DIRECTIVE", directive)
assert repr(matrix) == (
"<Matrix: {'foo': ('a', 'b', 'c'), "
"'DIRECTIVE': ({'foo': 'a', 'bar': '1'}, {'foo': 'b', 'bar': '2'})}>"
).replace("DIRECTIVE", directive)
# Multiple insertions.
add({"foo": "c", "bar": "3"}, {"foo": "d", "bar": "4"})
assert matrix.matrix() == {
"foo": ("a", "b", "c"),
directive: (
{"foo": "a", "bar": "1"},
{"foo": "b", "bar": "2"},
{"foo": "c", "bar": "3"},
{"foo": "d", "bar": "4"},
),
}
# Forbidden values.
with pytest.raises(ValueError):
add({"include": "random"})
with pytest.raises(ValueError):
add({"exclude": "random"})
[docs]
def test_all_variations():
"""`all_variations` reports every axis and its values."""
matrix = Matrix()
assert matrix.all_variations() == {}
matrix.add_variation("foo", ["a", "b", "c"])
matrix.add_variation("bar", ["1", "2", "3"])
matrix.add_includes(
{"foo": "b", "color": "green"},
{"foo": "d", "color": "orange"},
{"bar": "1", "shape": "triangle"},
{"size": "small"},
)
matrix.add_excludes(
{"foo": "b", "shape": "circle"},
{"bar": "2", "color": "blue"},
{"bar": "4", "color": "yellow"},
{"weight": "heavy"},
)
assert matrix.matrix() == {
"foo": ("a", "b", "c"),
"bar": ("1", "2", "3"),
"include": (
{"foo": "b", "color": "green"},
{"foo": "d", "color": "orange"},
{"bar": "1", "shape": "triangle"},
{"size": "small"},
),
"exclude": (
{"foo": "b", "shape": "circle"},
{"bar": "2", "color": "blue"},
{"bar": "4", "color": "yellow"},
{"weight": "heavy"},
),
}
assert matrix.all_variations() == {
"foo": ("a", "b", "c"),
"bar": ("1", "2", "3"),
}
assert matrix.all_variations(with_includes=True) == {
"foo": ("a", "b", "c", "d"),
"bar": ("1", "2", "3"),
"color": ("green", "orange"),
"shape": ("triangle",),
"size": ("small",),
}
[docs]
def test_product():
"""`product` is the plain cartesian product, before include/exclude."""
matrix = Matrix()
assert tuple(matrix.product()) == ()
matrix.add_variation("foo", ["a", "b"])
matrix.add_variation("bar", ["1", "2"])
assert tuple(matrix.product()) == (
{"foo": "a", "bar": "1"},
{"foo": "a", "bar": "2"},
{"foo": "b", "bar": "1"},
{"foo": "b", "bar": "2"},
)
matrix.add_includes(
{"foo": "b", "baz": "W"},
{"foo": "c", "baz": "X"},
{"bar": "1", "qux": "福"},
{"@": "$"},
)
matrix.add_excludes(
{"foo": "b", "qux": "子"},
{"bar": "2", "baz": "Y"},
{"bar": "3", "baz": "Z"},
{"E": "O"},
)
assert tuple(matrix.product()) == (
{"foo": "a", "bar": "1"},
{"foo": "a", "bar": "2"},
{"foo": "b", "bar": "1"},
{"foo": "b", "bar": "2"},
)
[docs]
def test_solve_includes_are_order_sensitive():
"""Later includes overwrite non-axis values added by earlier ones.
GitHub applies includes sequentially, so `{color: green}` (added to every
combination) overwrites the `color: pink` an earlier, more specific include
set on the cat combinations. Reversing the two would keep pink. See
{func}`test_solve_matches_github_include_example` for the documented order.
"""
matrix = Matrix()
matrix.add_variation("fruit", ["apple", "pear"])
matrix.add_variation("animal", ["cat", "dog"])
matrix.add_includes(
{"color": "pink", "animal": "cat"},
{"color": "green"},
)
assert tuple(matrix.solve()) == (
{"fruit": "apple", "animal": "cat", "color": "green"},
{"fruit": "apple", "animal": "dog", "color": "green"},
{"fruit": "pear", "animal": "cat", "color": "green"},
{"fruit": "pear", "animal": "dog", "color": "green"},
)
[docs]
def test_solve_expanded_configuration():
"""GitHub's documented include example: a matching include expands one cell."""
matrix = Matrix()
matrix.add_variation("os", ["windows-latest", "ubuntu-latest"])
matrix.add_variation("node", ["14", "16"])
matrix.add_includes({"os": "windows-latest", "node": "16", "npm": "6"})
assert tuple(matrix.solve()) == (
{"os": "windows-latest", "node": "14"},
{"os": "windows-latest", "node": "16", "npm": "6"},
{"os": "ubuntu-latest", "node": "14"},
{"os": "ubuntu-latest", "node": "16"},
)
[docs]
def test_solve_extended_configuration():
"""GitHub's documented include example: a non-matching include appends a job."""
matrix = Matrix()
matrix.add_variation("os", ["macos-latest", "windows-latest"])
matrix.add_variation("version", ["12", "14", "16"])
matrix.add_includes({"os": "windows-latest", "version": "17"})
assert tuple(matrix.solve()) == (
{"os": "macos-latest", "version": "12"},
{"os": "macos-latest", "version": "14"},
{"os": "macos-latest", "version": "16"},
{"os": "windows-latest", "version": "12"},
{"os": "windows-latest", "version": "14"},
{"os": "windows-latest", "version": "16"},
{"os": "windows-latest", "version": "17"},
)
[docs]
def test_solve_empty_matrix():
"""Includes alone define the jobs when no axis is declared."""
matrix = Matrix()
matrix.add_includes(
{"site": "production", "datacenter": "site-a"},
{"site": "staging", "datacenter": "site-b"},
)
assert tuple(matrix.solve()) == (
{"site": "production", "datacenter": "site-a"},
{"site": "staging", "datacenter": "site-b"},
)
[docs]
@pytest.mark.parametrize(
"excludes",
list(
permutations(
(
# The order of these 3 includes directives can be shuffled as the
# final result order is imposed by the base variations of the original
# matrix.
{"os": "macos-latest", "version": "12", "environment": "production"},
{"os": "windows-latest", "version": "16"},
),
2,
)
),
)
def test_solve_excludes(excludes):
"""Excludes remove matching cells, whatever order they are declared in."""
matrix = Matrix()
matrix.add_variation("os", ["macos-latest", "windows-latest"])
matrix.add_variation("version", ["12", "14", "16"])
matrix.add_variation("environment", ["staging", "production"])
matrix.add_excludes(*excludes)
assert tuple(matrix.solve()) == (
{"os": "macos-latest", "version": "12", "environment": "staging"},
# {"os": "macos-latest", "version": "12", "environment": "production"},
{"os": "macos-latest", "version": "14", "environment": "staging"},
{"os": "macos-latest", "version": "14", "environment": "production"},
{"os": "macos-latest", "version": "16", "environment": "staging"},
{"os": "macos-latest", "version": "16", "environment": "production"},
{"os": "windows-latest", "version": "12", "environment": "staging"},
{"os": "windows-latest", "version": "12", "environment": "production"},
{"os": "windows-latest", "version": "14", "environment": "staging"},
{"os": "windows-latest", "version": "14", "environment": "production"},
# {"os": "windows-latest", "version": "16", "environment": "staging"},
# {"os": "windows-latest", "version": "16", "environment": "production"},
)
[docs]
@pytest.mark.parametrize(
"excludes",
list(
permutations(
(
# The order of these 2 excludes directives can be shuffled as the
# final result order is imposed by the base variations of the original
# matrix.
{"os": "windows-latest", "version": "14"},
{"os": "linux-latest"},
),
2,
)
),
)
def test_solve_exclude_partial(excludes):
"""A partial exclude removes every cell sharing the named values."""
matrix = Matrix()
matrix.add_variation("os", ["macos-latest", "windows-latest", "linux-latest"])
matrix.add_variation("version", ["14", "16"])
matrix.add_excludes(*excludes)
assert tuple(matrix.solve()) == (
{"os": "macos-latest", "version": "14"},
{"os": "macos-latest", "version": "16"},
# {"os": "windows-latest", "version": "14"},
{"os": "windows-latest", "version": "16"},
# {"os": "linux-latest", "version": "14"},
# {"os": "linux-latest", "version": "16"},
)
[docs]
def test_solve_exclude_include_priority():
"""A partial include re-added after exclude is a single job, not a slice.
The include `{os: linux-latest}` matches no surviving combination (adding it
would overwrite the original `os`), so GitHub appends it verbatim as one new
job. It does not fan back out across the excluded `version` axis: that would
require the include to specify each `version` itself.
"""
matrix = Matrix()
matrix.add_variation("os", ["macos-latest", "windows-latest", "linux-latest"])
matrix.add_variation("version", ["14", "16"])
matrix.add_includes({"os": "linux-latest"})
matrix.add_excludes({"os": "linux-latest"})
assert tuple(matrix.solve()) == (
{"os": "macos-latest", "version": "14"},
{"os": "macos-latest", "version": "16"},
{"os": "windows-latest", "version": "14"},
{"os": "windows-latest", "version": "16"},
{"os": "linux-latest"},
)
[docs]
def test_solve_exclude_include_selectivity():
"""A fully-qualified include survives an exclude that removes its siblings."""
matrix = Matrix()
matrix.add_variation("os", ["macos-latest", "windows-latest", "linux-latest"])
matrix.add_variation("version", ["14", "16"])
matrix.add_includes({"os": "linux-latest", "version": "16", "node": "20"})
matrix.add_excludes({"os": "linux-latest"})
assert tuple(matrix.solve()) == (
{"os": "macos-latest", "version": "14"},
{"os": "macos-latest", "version": "16"},
{"os": "windows-latest", "version": "14"},
{"os": "windows-latest", "version": "16"},
# {"os": "linux-latest", "version": "14"},
{"os": "linux-latest", "version": "16", "node": "20"},
)
[docs]
def test_solve_exclude_matches_on_the_keys_it_shares():
"""An exclude naming a key outside the axes matches on the keys it shares."""
matrix = Matrix()
matrix.add_variation("os", ["macos-latest", "windows-latest", "linux-latest"])
matrix.add_variation("version", ["3.14", "3.13", "3.12"])
matrix.add_excludes(
{"os": "linux-latest", "unknown_key": "random"},
{"version": "3.14"},
)
assert tuple(matrix.solve()) == (
{"os": "macos-latest", "version": "3.13"},
{"os": "macos-latest", "version": "3.12"},
{"os": "windows-latest", "version": "3.13"},
{"os": "windows-latest", "version": "3.12"},
)
[docs]
def test_solve_reports_a_matrix_past_the_job_limit(caplog):
"""GitHub refuses a matrix past its job limit, so the solver says so."""
matrix = Matrix()
matrix.add_variation("index", [str(i) for i in range(GITHUB_MATRIX_JOB_LIMIT)])
assert len(tuple(matrix.solve())) == GITHUB_MATRIX_JOB_LIMIT
assert not caplog.records
matrix.add_variation("index", ["one-more"])
assert len(tuple(matrix.solve())) == GITHUB_MATRIX_JOB_LIMIT + 1
assert "past the GitHub limit" in caplog.text
[docs]
def test_job_keys_reads_the_solved_jobs():
"""A key only an include sets is listed; an axis no job keeps is not."""
matrix = Matrix()
matrix.add_variation("fruit", ["apple", "pear"])
matrix.add_includes({"color": "green"})
assert matrix.job_keys() == ("color", "fruit")
matrix.add_excludes({"fruit": "apple"}, {"fruit": "pear"})
# Every combination is excluded, and the include merges into nothing, so
# it is appended as a job of its own.
assert matrix.job_keys() == ("color",)
[docs]
def test_pivot_counts_marks_an_unstable_row():
"""A partial state include flags the matching row's cells unstable."""
matrix = Matrix()
matrix.add_variation("os", ["ubuntu-slim", "macos-26"])
matrix.add_variation("python-version", ["3.14", "3.15"])
matrix.add_includes({"state": "stable"})
matrix.add_includes({"state": "unstable", "python-version": "3.15"})
assert matrix.pivot_counts() == {
("3.14", "ubuntu-slim"): Counter({"stable": 1}),
("3.14", "macos-26"): Counter({"stable": 1}),
("3.15", "ubuntu-slim"): Counter({"unstable": 1}),
("3.15", "macos-26"): Counter({"unstable": 1}),
}
[docs]
def test_pivot_counts_tallies_the_jobs_behind_each_cell():
"""A cell collapsing several jobs counts them, per distinct state."""
matrix = Matrix()
matrix.add_variation("os", ["ubuntu-slim"])
matrix.add_variation("python-version", ["3.14"])
matrix.add_variation("click-version", ["stable", "main", "released"])
matrix.add_includes({"state": "stable"})
matrix.add_includes({"state": "unstable", "click-version": "main"})
assert matrix.pivot_counts() == {
("3.14", "ubuntu-slim"): Counter({"stable": 2, "unstable": 1})
}
[docs]
def test_pivot_counts_omits_an_empty_intersection():
"""An excluded combination is absent, not present with an empty counter."""
matrix = Matrix()
matrix.add_variation("os", ["ubuntu-slim", "macos-26"])
matrix.add_variation("python-version", ["3.10", "3.14"])
matrix.add_includes({"state": "stable"})
matrix.add_excludes({"os": "macos-26", "python-version": "3.14"})
tallies = matrix.pivot_counts()
assert ("3.14", "macos-26") not in tallies
assert set(tallies) == {
("3.10", "ubuntu-slim"),
("3.10", "macos-26"),
("3.14", "ubuntu-slim"),
}
assert all(tally == Counter({"stable": 1}) for tally in tallies.values())
[docs]
def test_pivot_counts_on_custom_axes():
"""The counts lay out any two axes, tallying any cell key."""
matrix = Matrix()
matrix.add_variation("fruit", ["apple", "pear"])
matrix.add_variation("city", ["paris", "tokyo"])
matrix.add_includes({"taste": "sweet"})
matrix.add_excludes({"fruit": "pear", "city": "tokyo"})
assert matrix.pivot_counts(row_axis="fruit", col_axis="city", cell_key="taste") == {
("apple", "paris"): Counter({"sweet": 1}),
("apple", "tokyo"): Counter({"sweet": 1}),
("pear", "paris"): Counter({"sweet": 1}),
}