diff --git a/tests/features/test_cname_main.py b/tests/features/test_cname_main.py new file mode 100644 index 00000000..46ce9cd4 --- /dev/null +++ b/tests/features/test_cname_main.py @@ -0,0 +1,178 @@ +import sys +import types +import logging +import pytest + +import gardenlinux.features.cname_main as cname_main + + +def test_main_happy(monkeypatch, capsys): + """ + Test the "Happy Path" of the main() function. + """ + # Arrange + argv = ["prog", "--arch", "amd64", "--version", "1.0-abc123", "flav-amd64"] + monkeypatch.setattr(sys, "argv", argv) + + class FakeCName: + def __init__(self, cname, arch=None, version=None): + self.arch = arch + self.flavor = "flav" + self.version_and_commit_id = "1.0-abc123" + + monkeypatch.setattr(cname_main, "CName", FakeCName) + + class FakeGraph: + in_degree = lambda self: [("f1", 0)] + edges = [("f1", "f2")] + + class FakeParser: + def __init__(self, *a, **k): + pass + + def filter(self, *a, **k): + return FakeGraph() + + @staticmethod + def sort_graph_nodes(graph): + return ["f1", "f2"] + + monkeypatch.setattr(cname_main, "Parser", FakeParser) + + # Act + cname_main.main() + + # Assert + out = capsys.readouterr().out + assert "f1" in out + assert "amd64" in out + + +def test_main_version_from_file(monkeypatch, capsys): + """ + "Happy Path" test for grabbing the version and commit id from file in main(). + """ + # Arrange + argv = ["prog", "--arch", "amd64", "flav-amd64"] + monkeypatch.setattr(sys, "argv", argv) + + monkeypatch.setattr( + cname_main, + "get_version_and_commit_id_from_files", + lambda root: ("2.0", "abcdef12"), + ) + + class FakeCName: + def __init__(self, cname, arch=None, version=None): + self.arch = arch + self.flavor = "flav" + self.version_and_commit_id = version + + monkeypatch.setattr(cname_main, "CName", FakeCName) + + class FakeParser: + def __init__(self, *a, **k): + pass + + def filter(self, *a, **k): + return types.SimpleNamespace(in_degree=lambda: [("f1", 0)], edges=[]) + + @staticmethod + def sort_graph_nodes(graph): + return ["f1"] + + monkeypatch.setattr(cname_main, "Parser", FakeParser) + + # Act + cname_main.main() + + # Assert + assert "2.0-abcdef12" in capsys.readouterr().out + + +def test_cname_main_version_file_missing_warns(monkeypatch, caplog): + """ + Check if a warning is logged when it fails to read version and commit id files. + + Specifically, this test simulates a scenario where the helper function + `get_version_and_commit_id_from_files` raises a RuntimeError, which would occur + if the expected version or commit files are missing or unreadable. + """ + # Arrange + argv = ["prog", "--arch", "amd64", "flav-amd64"] + monkeypatch.setattr(sys, "argv", argv) + + # Patch version fatch function to raise RuntimeError (Simulates missing files) + def raise_runtime(_): + raise RuntimeError("missing") + + monkeypatch.setattr( + cname_main, "get_version_and_commit_id_from_files", raise_runtime + ) + + # Patch CName to control attributes + class FakeCName: + def __init__(self, cname, arch=None, version=None): + self.arch = arch + self.flavor = "flav" + self.version_and_commit_id = version + + monkeypatch.setattr(cname_main, "CName", FakeCName) + + # Patch Parser for minimal valid graph + class FakeParser: + def __init__(self, *a, **k): + pass + + # Return object with in_degree method returning a node with zero dependencies + def filter(self, *a, **k): + return types.SimpleNamespace(in_degree=lambda: [("f1", 0)], edges=[]) + + @staticmethod + def sort_graph_nodes(graph): + return ["f1"] + + monkeypatch.setattr(cname_main, "Parser", FakeParser) + + # Capture any logs with WARNING level + caplog.set_level(logging.WARNING) + + # Act + cname_main.main() + + # Assert + assert "Failed to parse version information" in caplog.text + + +def test_cname_main_invalid_cname_raises(monkeypatch): + """ + Test if AssertionError is raised with an invalid or malformed cname. + """ + # Arrange + argv = ["prog", "--arch", "amd64", "--version", "1.0", "INVALID@NAME"] + monkeypatch.setattr(sys, "argv", argv) + + # Act / Assert + with pytest.raises(AssertionError): + cname_main.main() + + +def test_cname_main_missing_arch_in_cname_raises(monkeypatch): + """ + Test if an assertion error is raised when the arch argument is missing. + """ + # Arrange + argv = ["prog", "--version", "1.0", "flav"] + monkeypatch.setattr(sys, "argv", argv) + + class FakeCName: + def __init__(self, cname, arch=None, version=None): + self.arch = None # Force missing arch + self.flavor = "flav" + self.version_and_commit_id = "1.0-abc" + + monkeypatch.setattr(cname_main, "CName", FakeCName) + + # Act / Assert + with pytest.raises(AssertionError): + cname_main.main() diff --git a/tests/features/test_main.py b/tests/features/test_main.py new file mode 100644 index 00000000..71e1d4f0 --- /dev/null +++ b/tests/features/test_main.py @@ -0,0 +1,275 @@ +import sys +import types +import pytest + +import gardenlinux.features.__main__ as fema + +# ------------------------------- +# Helper function tests +# ------------------------------- + + +def test_get_cname_base(): + # Arrange + sorted_features = ["base", "_hidden", "extra"] + + # Act + result = fema.get_cname_base(sorted_features) + + # Assert + assert result == "base_hidden-extra" + + +def test_get_cname_base_empty_raises(): + # get_cname_base with empty iterable raises TypeError + with pytest.raises(TypeError): + fema.get_cname_base([]) + + +def test_sort_return_intersection_subset(): + # Arrange + input_set = {"a", "c"} + order_list = ["a", "b", "c", "d"] + + # Act + result = fema.sort_subset(input_set, order_list) + + # Assert + assert result == ["a", "c"] + + +def test_sort_subset_nomatch(): + # Arrange + input_set = {"x", "y"} + order_list = ["a", "b", "c"] + + # Act + result = fema.sort_subset(input_set, order_list) + + # Assert + assert result == [] + + +def test_sort_subset_with_empty_order_list(): + # Arrange + input_set = {"a", "b"} + order_list = [] + + result = fema.sort_subset(input_set, order_list) + + assert result == [] + + +def test_graph_mermaid(): + # Arrange + class FakeGraph: + edges = [("a", "b"), ("b", "c")] + + flavor = "test" + + # Act + markup = fema.graph_as_mermaid_markup(flavor, FakeGraph()) + + # Assert + assert "graph TD" in markup + assert "a-->b" in markup + assert "b-->c" in markup + + +def test_get_minimal_feature_set_filters(): + # Arrange + class FakeGraph: + def in_degree(self): + return [("a", 0), ("b", 1), ("c", 0)] + + graph = FakeGraph() + + # Act + result = fema.get_minimal_feature_set(graph) + + # Assert + assert result == {"a", "c"} + + +def test_get_version_and_commit_from_file(tmp_path): + # Arrange + commit_file = tmp_path / "COMMIT" + commit_file.write_text("abcdef12\n") + version_file = tmp_path / "VERSION" + version_file.write_text("1.2.3\n") + + # Act + version, commit = fema.get_version_and_commit_id_from_files(str(tmp_path)) + + # Arrange + assert version == "1.2.3" + assert commit == "abcdef12" + + +def test_get_version_missing_file_raises(tmp_path): + # Arrange (one file only) + (tmp_path / "COMMIT").write_text("abcdef1234\n") + + # Act / Assert + with pytest.raises(RuntimeError): + fema.get_version_and_commit_id_from_files(str(tmp_path)) + + +# ------------------------------- +# Tests for main() +# ------------------------------- +def test_main_prints_arch(monkeypatch, capsys): + # Arrange + argv = ["prog", "--arch", "amd64", "--features", "f1", "--version", "1.0", "arch"] + monkeypatch.setattr(sys, "argv", argv) + monkeypatch.setattr(fema, "Parser", lambda *a, **kw: None) + + # Act + fema.main() + + # Assert + out = capsys.readouterr().out + assert "amd64" in out + + +def test_main_prints_flags_elements_platforms(monkeypatch, capsys): + # Arrange + argv = [ + "prog", + "--arch", + "amd64", + "--features", + "flag1,element1,platform1", + "--version", + "1.0", + "flags", + ] + monkeypatch.setattr(sys, "argv", argv) + + class FakeParser: + def __init__(self, *a, **k): + pass + + @staticmethod + def filter_as_dict(*a, **k): + return { + "flag": ["flag1"], + "element": ["element1"], + "platform": ["platform1"], + } + + monkeypatch.setattr(fema, "Parser", FakeParser) + + # Act + fema.main() + + # Assert + out = capsys.readouterr().out + assert "flag1" in out + + +def test_main_prints_version(monkeypatch, capsys): + # Arrange + argv = ["prog", "--arch", "amd64", "--features", "f1", "version"] + monkeypatch.setattr(sys, "argv", argv) + monkeypatch.setattr( + fema, + "Parser", + lambda *a, **kw: types.SimpleNamespace(filter=lambda *a, **k: None), + ) + # Patch get_version_and_commit_id_from_files + monkeypatch.setattr( + fema, "get_version_and_commit_id_from_files", lambda root: ("1.2.3", "abcdef12") + ) + + # Act + fema.main() + + captured = capsys.readouterr() + assert "1.2.3-abcdef12" in captured.out + + +def test_main_arch_raises_missing_verison(monkeypatch, capsys): + # Arrange + argv = ["prog", "--arch", "amd64", "--features", "f1", "arch"] + monkeypatch.setattr(sys, "argv", argv) + monkeypatch.setattr(fema, "Parser", lambda *a, **kw: None) + + # Act / Assert + with pytest.raises(RuntimeError): + fema.main() + + +def test_main_with_cname_print_cname(monkeypatch, capsys): + # Arrange + class FakeCName: + def __init__(self, cname, arch=None, version=None): + self.arch = arch + self.flavor = "flav" + self.commit_id = "abc123" + self.version = version + + monkeypatch.setattr(fema, "CName", FakeCName) + + class FakeGraph: + def in_degree(self): + # Simulate a graph where one feature has no dependencies + return [("f1", 0)] + + class FakeParser: + def __call__(self, *a, **k): + return types.SimpleNamespace(filter=lambda *a, **k: FakeGraph()) + + @staticmethod + def get_cname_as_feature_set(f): + return {"f1"} + + @staticmethod + def sort_graph_nodes(graph): + return ["f1"] + + @staticmethod + def sort_subset(subset, length): + return [] + + monkeypatch.setattr(fema, "Parser", FakeParser()) + + monkeypatch.setattr( + sys, + "argv", + ["prog", "--cname", "flav", "--arch", "amd64", "--version", "1.0", "cname"], + ) + + # Act + fema.main() + + # Assert + captured = capsys.readouterr() + assert "flav" in captured.out + + +def test_main_requires_feature_or_cname(monkeypatch): + # Arrange + monkeypatch.setattr(sys, "argv", ["prog", "arch"]) + monkeypatch.setattr(fema, "Parser", lambda *a, **kw: None) + + # Act / Assert + with pytest.raises(AssertionError): + fema.main() + + +def test_main_raises_no_arch_no_default(monkeypatch): + # Arrange + # args.type == 'cname, arch is None and no default_arch set + argv = ["prog", "--features", "f1", "cname"] + monkeypatch.setattr(sys, "argv", argv) + monkeypatch.setattr( + fema, + "Parser", + lambda *a, **kw: types.SimpleNamesapce(filter=lambda *a, **k: None), + ) + monkeypatch.setattr(fema, "CName", lambda *a, **kw: None) + + # Act / Assert + with pytest.raises(RuntimeError, match="Architecture could not be determined"): + fema.main()