From 65254031f09462ac2ce9d909863e55a09ad20247 Mon Sep 17 00:00:00 2001 From: Sam Clarke-Green Date: Thu, 14 Aug 2025 17:42:23 +0100 Subject: [PATCH] Add compiler selection arguments Add some standard flags for compiler selection, with environment variables as potential fallbacks. Adds tests for the new options and add tests for two previously untested error conditions to bring test coverage up to 100 percent. --- source/fab/cui/arguments.py | 37 ++++++++- tests/unit_tests/test_cui_arguments.py | 105 ++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/source/fab/cui/arguments.py b/source/fab/cui/arguments.py index 39c931e7..61cbf45f 100644 --- a/source/fab/cui/arguments.py +++ b/source/fab/cui/arguments.py @@ -60,6 +60,7 @@ def inner(self, *args, **kwargs): self._setup_needed = False self._add_location_group() self._add_output_group() + self._add_compiler_group() self._add_info_group() result = func(self, *args, **kwargs) @@ -87,6 +88,11 @@ def inner(self, *args, **kwargs): class FabArgumentParser(argparse.ArgumentParser): """Fab command argument parser.""" + # Fallback compiler family names + _default_cc = "gcc" + _default_cxx = "g++" + _default_fc = "gfortran" + def __init__(self, *args, **kwargs): self.version = kwargs.pop("version", str(fab_version)) @@ -171,13 +177,42 @@ def _add_info_group(self): """Add informative options.""" # Create an info group - group = self.add_argument_group("info arguments") + group = self.add_argument_group("fab info arguments") if "--version" not in self._option_string_actions: group.add_argument( "--version", action="version", version=f"%(prog)s {self.version}" ) + def _add_compiler_group(self): + """Add compiler options.""" + + group = self.add_argument_group("fab compiler arguments") + + if "--cc" not in self._option_string_actions: + group.add_argument( + "--cc", + type=str, + default=os.environ.get("CC", self._default_cc), + help="name of the C compiler (default: %(default)s)", + ) + + if "--cxx" not in self._option_string_actions: + group.add_argument( + "--cxx", + type=str, + default=os.environ.get("CXX", self._default_cxx), + help="name of the C++ compiler (default: %(default)s)", + ) + + if "--fc" not in self._option_string_actions: + group.add_argument( + "--fc", + type=str, + default=os.environ.get("FC", self._default_fc), + help="name of the Fortran compiler (default: %(default)s)", + ) + def _configure_logging(self, namespace: argparse.Namespace) -> None: """Configure output logging. diff --git a/tests/unit_tests/test_cui_arguments.py b/tests/unit_tests/test_cui_arguments.py index 8e86cf21..bd10b7a9 100644 --- a/tests/unit_tests/test_cui_arguments.py +++ b/tests/unit_tests/test_cui_arguments.py @@ -18,6 +18,7 @@ import pytest from typing import Optional from pyfakefs.fake_filesystem import FakeFilesystem +from unittest.mock import Mock class TestFullPathType: @@ -123,6 +124,23 @@ def test_no_help(self): assert isinstance(args, argparse.Namespace) assert args.file is None + def test_error_handling(self, monkeypatch): + """Check the ArgumentError exception handling.""" + + parser = FabArgumentParser() + + def replacement(*args, **kwargs): + raise argparse.ArgumentError( + Mock(option_strings=["--test"]), "error testing" + ) + + monkeypatch.setattr(argparse.ArgumentParser, "parse_known_args", replacement) + + args = parser.parse_fabfile_only([]) + assert isinstance(args, argparse.Namespace) + assert args.file is None + assert args.zero_config + class TestParser: """Test the core parser and its default options.""" @@ -214,7 +232,9 @@ def test_outupt_with_quiet(self, argv, capsys): pytest.param( [], None, Path("fab-workspace").expanduser().resolve(), id="default" ), - pytest.param([], Path("/tmp/fab"), Path("/tmp/fab").resolve(), id="environment"), + pytest.param( + [], Path("/tmp/fab"), Path("/tmp/fab").resolve(), id="environment" + ), pytest.param( ["--workspace", "/run/fab"], Path("/tmp/fab"), @@ -296,3 +316,86 @@ def test_version(self, capsys, monkeypatch): captured = capsys.readouterr() assert captured.out.startswith("fab ") + + def test_error_handling(self, monkeypatch): + """Check the ArgumentError exception handling.""" + + parser = FabArgumentParser() + + def replacement(*args, **kwargs): + return None, None + + monkeypatch.setattr(argparse.ArgumentParser, "parse_args", replacement) + + with pytest.raises(ValueError) as exc: + parser.parse_args(["--version"]) + assert "invalid return value from wrapped function" in str(exc.value) + + +class TestCompilerFlags: + """Test the compiler target flags.""" + + def test_defaults(self, monkeypatch): + """Test class default settings.""" + + if "CC" in os.environ: + monkeypatch.delenv("CC") + if "CXX" in os.environ: + monkeypatch.delenv("CXX") + if "FC" in os.environ: + monkeypatch.delenv("FC") + + parser = FabArgumentParser() + args = parser.parse_args([]) + + assert args.cc == "gcc" + assert args.cxx == "g++" + assert args.fc == "gfortran" + + @pytest.mark.parametrize( + "argv,env,cc,cxx,fc", + [ + pytest.param(["--cc", "icc"], {}, "icc", "g++", "gfortran", id="cc flag"), + pytest.param(["--cxx", "icx"], {}, "gcc", "icx", "gfortran", id="cxx flag"), + pytest.param(["--fc", "ifx"], {}, "gcc", "g++", "ifx", id="fc flag"), + pytest.param([], {"CC": "icc"}, "icc", "g++", "gfortran", id="CC env"), + pytest.param([], {"CXX": "icx"}, "gcc", "icx", "gfortran", id="CXX env"), + pytest.param([], {"FC": "ifx"}, "gcc", "g++", "ifx", id="FC env"), + pytest.param( + ["--cc", "cc"], + {"CC": "icc"}, + "cc", + "g++", + "gfortran", + id="cc flag+env", + ), + pytest.param( + ["--cxx", "CC"], + {"CXX": "icc"}, + "gcc", + "CC", + "gfortran", + id="cxx flag+env", + ), + pytest.param( + ["--fc", "ftn"], + {"FC": "ifx"}, + "gcc", + "g++", + "ftn", + id="FC flag+env", + ), + ], + ) + def test_from_env(self, argv, env, cc, cxx, fc, monkeypatch): + """Check compiler settings and environment overrides.""" + + for key, value in env.items(): + monkeypatch.setenv(key, value) + + parser = FabArgumentParser() + args = parser.parse_args(argv) + + assert args.cc == cc + assert args.cxx == cxx + assert args.fc == fc