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