From 3c6b7c03bc76305e52dc630ef4721c4d58dfdb66 Mon Sep 17 00:00:00 2001 From: Tiara Lena Hock Date: Mon, 11 Aug 2025 14:49:12 +0200 Subject: [PATCH] Add tests for src/flavors module --- tests/flavors/__init__.py | 0 tests/flavors/test_init.py | 36 ++++++ tests/flavors/test_main.py | 226 +++++++++++++++++++++++++++++++++++ tests/flavors/test_parser.py | 159 ++++++++++++++++++++++++ 4 files changed, 421 insertions(+) create mode 100644 tests/flavors/__init__.py create mode 100644 tests/flavors/test_init.py create mode 100644 tests/flavors/test_main.py create mode 100644 tests/flavors/test_parser.py diff --git a/tests/flavors/__init__.py b/tests/flavors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/flavors/test_init.py b/tests/flavors/test_init.py new file mode 100644 index 00000000..260e0f0f --- /dev/null +++ b/tests/flavors/test_init.py @@ -0,0 +1,36 @@ +import types +import importlib + +from gardenlinux import flavors + + +def test_parser_exposed_at_top_level(): + """Parser should be importable directly from the package.""" + from gardenlinux.flavors import parser + + assert flavors.Parser is parser.Parser + + +def test___all___is_correct(): + """__all__ should only contain Parser.""" + assert flavors.__all__ == ["Parser"] + + +def test_star_import(monkeypatch): + """from flavors import * should bring Parser into locals().""" + # Arrange + namespace = {} + + # Act + module = importlib.import_module("gardenlinux.flavors") + for name in getattr(module, "__all__", []): + namespace[name] = getattr(module, name) + + # Assert + assert "Parser" in namespace + assert namespace["Parser"] is flavors.Parser + + +def test_import_module(): + """Importing the package should not raise exceptions.""" + importlib.reload(flavors) # Should succeed without errors diff --git a/tests/flavors/test_main.py b/tests/flavors/test_main.py new file mode 100644 index 00000000..274f22a4 --- /dev/null +++ b/tests/flavors/test_main.py @@ -0,0 +1,226 @@ +import json +import sys +import pytest + +import gardenlinux.flavors.__main__ as fm + + +def test_generate_markdown_table(): + # Arrange + combos = [("amd64", "linux-amd64")] + + # Act + table = fm.generate_markdown_table(combos, no_arch=False) + + # Assert + assert table.startswith("| Platform | Architecture | Flavor") + assert "`linux-amd64`" in table + assert "| linux" + + +def test_parse_args(monkeypatch): + """simulate CLI invocation and make sure parse_args reads them correctly""" + # Arrange + argv = [ + "prog", + "--no-arch", + "--include-only", + "a*", + "--exclude", + "b*", + "--build", + "--publish", + "--test", + "--test-platform", + "--category", + "cat1", + "--exclude-category", + "cat2", + "--json-by-arch", + ] + monkeypatch.setattr(sys, "argv", argv) + + # Act + args = fm.parse_args() + + # Assert + assert args.no_arch is True + assert args.include_only == ["a*"] + assert args.exclude == ["b*"] + assert args.build is True + assert args.publish is True + assert args.test is True + assert args.test_platform is True + assert args.category == ["cat1"] + assert args.exclude_category == ["cat2"] + assert args.json_by_arch is True + + +def _make_parser_class(filter_result, group_result=None, remove_result=None): + """ + Factory to create a fake Parser class + Instances ignore the favors_data passed to __init__. + """ + + class DummyParser: + def __init__(self, flavors_data): + self._data = flavors_data + + def filter(self, **kwargs): + # return the prepared combinations list + return filter_result + + @staticmethod + def group_by_arch(combinations): + # Return a prepared mapping or derive a simple mapping if None + if group_result is not None: + return group_result + # naive default behaviour: group combinations by arch + d = {} + for arch, comb in combinations: + d.setdefault(arch, []).append(comb) + return d + + @staticmethod + def remove_arch(combinations): + if remove_result is not None: + return remove_result + # naive default: remote '-{arch}' suffix if present + out = [] + for arch, comb in combinations: + suffix = f"-{arch}" + if comb.endswith(suffix): + out.append(comb[: -len(suffix)]) + else: + out.append(comb) + return out + + return DummyParser + + +def test_main_exits_when_flavors_missing(tmp_path, monkeypatch): + # Arrange + # make Git().root point to a tmp dir that does NOT contain flavors.yaml + class DummyGit: + def __init__(self): + self.root = str(tmp_path) + + monkeypatch.setattr(fm, "Git", DummyGit) + + # ensure no flavors.yaml + monkeypatch.setattr(sys, "argv", ["prog"]) + + # Act / Assert + with pytest.raises(SystemExit) as excinfo: + fm.main() + assert "does not exist" in str(excinfo.value) + + +def test_main_json_by_arch_prints_json(tmp_path, monkeypatch, capsys): + # Arrange + # prepare flavors.yaml at tmp path + flavors_file = tmp_path / "flavors.yaml" + flavors_file.write_text("dummy: content") + + class DummyGit: + def __init__(self): + self.root = str(tmp_path) + + # define combinations and expected grouped mapping + combinations = [("x86", "linux-x86"), ("arm", "android-arm")] + grouped = {"x86": ["linux-x86"], "arm": ["android-arm"]} + + DummyParser = _make_parser_class(filter_result=combinations, group_result=grouped) + monkeypatch.setattr(fm, "Git", DummyGit) + monkeypatch.setattr(fm, "Parser", DummyParser) + monkeypatch.setattr(sys, "argv", ["prog", "--json-by-arch"]) + + # Act + fm.main() + out = capsys.readouterr().out + + # Assert + parsed = json.loads(out) + assert parsed == grouped + + +def test_main_json_by_arch_with_no_arch_strips_arch_suffix( + tmp_path, monkeypatch, capsys +): + # Arrange + flavors_file = tmp_path / "flavors.yaml" + flavors_file.write_text("dummy: content") + + class DummyGit: + def __init__(self): + self.root = str(tmp_path) + + combinations = [("x86", "linux-x86"), ("arm", "android-arm")] + # group_by_arch returns items that include architecture suffixes + grouped = {"x86": ["linux-x86"], "arm": ["android-arm"]} + DummyParser = _make_parser_class(filter_result=combinations, group_result=grouped) + + monkeypatch.setattr(fm, "Git", DummyGit) + monkeypatch.setattr(fm, "Parser", DummyParser) + monkeypatch.setattr(sys, "argv", ["prog", "--json-by-arch", "--no-arch"]) + + # Act + fm.main() + out = capsys.readouterr().out + + # Assert + parsed = json.loads(out) + # with --no-arch, main removes '-' from each flavor string + assert parsed == {"x86": ["linux"], "arm": ["android"]} + + +def test_main_markdown_table_branch(tmp_path, monkeypatch, capsys): + # Arrange + flavors_file = tmp_path / "flavors.yaml" + flavors_file.write_text("dummy: content") + + class DummyGit: + def __init__(self): + self.root = str(tmp_path) + + combinations = [("x86_64", "linux-x86_64"), ("armv7", "android-armv7")] + DummyParser = _make_parser_class(filter_result=combinations) + + monkeypatch.setattr(fm, "Git", DummyGit) + monkeypatch.setattr(fm, "Parser", DummyParser) + monkeypatch.setattr(sys, "argv", ["prog", "--markdown-table-by-platform"]) + + # Act + fm.main() + out = capsys.readouterr().out + + # Assert + assert "`linux-x86_64`" in out + assert "`android-armv7`" in out + assert "| Platform" in out + + +def test_main_default_prints_flavors_list(tmp_path, monkeypatch, capsys): + # Arrange + flavors_file = tmp_path / "flavors.yaml" + flavors_file.write_text("dummy: content") + + class DummyGit: + def __init__(self): + self.root = str(tmp_path) + + # filter returns tuples; main's default branch prints comb[1] values, sorted unique + combinations = [("x86", "linux-x86"), ("arm", "android-arm")] + DummyParser = _make_parser_class(filter_result=combinations) + + monkeypatch.setattr(fm, "Git", DummyGit) + monkeypatch.setattr(fm, "Parser", DummyParser) + monkeypatch.setattr(sys, "argv", ["prog"]) + + # Act + fm.main() + out = capsys.readouterr().out + lines = out.strip().splitlines() + + # Assert + assert sorted(lines) == sorted(["linux-x86", "android-arm"]) diff --git a/tests/flavors/test_parser.py b/tests/flavors/test_parser.py new file mode 100644 index 00000000..672af134 --- /dev/null +++ b/tests/flavors/test_parser.py @@ -0,0 +1,159 @@ +import yaml +import types +import pytest + +from gardenlinux.flavors.parser import Parser + + +@pytest.fixture +def valid_data(): + """Minimal data for valid GL_FLAVORS_SCHEMA.""" + return { + "targets": [ + { + "name": "linux", + "category": "cat1", + "flavors": [ + { + "features": ["f1"], + "arch": "amd64", + "build": True, + "test": True, + "test-platform": False, + "publish": True, + }, + { + "features": [], + "arch": "arm64", + "build": False, + "test": False, + "test-platform": False, + "publish": False, + }, + ], + }, + { + "name": "android", + "category": "cat2", + "flavors": [ + { + "features": ["f2"], + "arch": "arm64", + "build": False, + "test": False, + "test-platform": True, + "publish": False, + } + ], + }, + ] + } + + +def make_parser(data): + """ + Construct Parser from dict. + """ + return Parser(data) + + +def test_init_accepts_yaml_and_dict(valid_data): + # Arrange + yaml_str = yaml.safe_dump(valid_data) + + # Act + p_from_dict = Parser(valid_data) + p_from_yaml = Parser(yaml_str) + + # Assert + assert p_from_dict._flavors_data == valid_data + assert p_from_yaml._flavors_data == valid_data + + +def test_filter_defaults(valid_data): + # Arrange + parser = make_parser(valid_data) + + # Act + combos = parser.filter() + combo_names = [c[1] for c in combos] + + # Assert + assert any("linux-f1-amd64" in name for name in combo_names) + assert any("linux-arm64" in name for name in combo_names) + + +def test_filter_category_and_exclude(valid_data): + # Arrange + parser = make_parser(valid_data) + + # Act + linux_combos = parser.filter(filter_categories=["cat1"]) + android_combos = parser.filter(exclude_categories=["cat1"]) + + # Assert + assert all("linux" in name for _, name in linux_combos) + assert all("android" in name for _, name in android_combos) + + +@pytest.mark.parametrize("flag", ["only_build", "only_test", "only_publish"]) +def test_filter_with_flags(valid_data, flag): + # Arrange + parser = make_parser(valid_data) + + # Act + combos = parser.filter(**{flag: True}) + + # Assert + assert all("linux-f1-amd64" in name for _, name in combos) + + +def test_filter_only_test_platform(valid_data): + # Arrange + parser = make_parser(valid_data) + + # Act + combos = parser.filter(only_test_platform=True) + + # Assert + assert combos == [("arm64", "android-f2-arm64")] + + +def test_filter_with_excludes(valid_data): + # Arrange + parser = make_parser(valid_data) + + # Act + combos = parser.filter(wildcard_excludes=["linux*"]) + + # Assert + assert all(not name.startswith("linux") for _, name in combos) + + +def test_group_by_arch_and_remove_arch(): + # Arrange + combos = [ + ("amd64", "linux-amd64"), + ("arm64", "android-arm64"), + ("amd64", "foo-amd64"), + ] + + # Act + grouped = Parser.group_by_arch(combos) + removed = Parser.remove_arch(combos) + + # Assert + assert grouped["amd64"] == ["foo-amd64", "linux-amd64"] + assert grouped["arm64"] == ["android-arm64"] + assert "linux" in removed and "android" in removed + + +def test_exclude_include_only(): + # Arrange / Act / Assert + assert Parser.should_exclude("abc", ["abc"], []) is True + assert Parser.should_exclude("abc", [], ["a*"]) is True + assert Parser.should_exclude("abc", [], ["z*"]) is False + + assert Parser.should_include_only("abc", ["a*"]) is True + assert Parser.should_include_only("zzz", ["a*"]) is False + assert Parser.should_include_only("abc", []) is True