diff --git a/source/fab/tools/psyclone.py b/source/fab/tools/psyclone.py index 424b3907..fa508d7a 100644 --- a/source/fab/tools/psyclone.py +++ b/source/fab/tools/psyclone.py @@ -100,7 +100,7 @@ def process(self, kernel_roots: Optional[List[Union[str, Path]]] = None, api: Optional[str] = None, ): - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments, too-many-branches '''Run PSyclone with the specified parameters. If PSyclone is used to transform existing Fortran files, `api` must be None, and the output file name is `transformed_file`. If PSyclone is using its DSL @@ -122,6 +122,10 @@ def process(self, if not self.is_available: raise RuntimeError("PSyclone is not available.") + # Convert the old style API nemo to be empty + if api and api.lower() == "nemo": + api = "" + if api: # API specified, we need both psy- and alg-file, but not # transformed file. @@ -143,7 +147,7 @@ def process(self, "alg_file is specified.") if not transformed_file: raise RuntimeError("PSyclone called without api, but " - "transformed_file it not specified.") + "transformed_file is not specified.") parameters: List[Union[str, Path]] = [] # If an api is defined in this call (or in the constructor) add it diff --git a/tests/unit_tests/tools/test_psyclone.py b/tests/unit_tests/tools/test_psyclone.py index 619c0260..5586c485 100644 --- a/tests/unit_tests/tools/test_psyclone.py +++ b/tests/unit_tests/tools/test_psyclone.py @@ -36,6 +36,7 @@ def test_psyclone_constructor(): assert psyclone.category == Category.PSYCLONE assert psyclone.name == "psyclone" assert psyclone.exec_name == "psyclone" + # pylint: disable=use-implicit-booleaness-not-comparison assert psyclone.flags == [] @@ -120,19 +121,82 @@ def test_psyclone_check_available_errors(): match="Unexpected version information for PSyclone: " "'PSyclone version: NOT_A_NUMBER.4.0'"): assert not psyclone.check_available() + # Also check that we can't call process if PSyclone is not available. + psyclone._is_available = False + config = mock.Mock() + with pytest.raises(RuntimeError) as err: + psyclone.process(config, "x90file") + assert "PSyclone is not available" in str(err.value) + + +def test_psyclone_processing_errors_without_api(): + '''Test all processing errors in PSyclone if no API is specified.''' + + psyclone = Psyclone() + psyclone._is_available = True + config = mock.Mock() + + # No API --> we need transformed file, but not psy or alg: + with pytest.raises(RuntimeError) as err: + psyclone.process(config, "x90file", api=None, psy_file="psy_file") + assert ("PSyclone called without api, but psy_file is specified" + in str(err.value)) + with pytest.raises(RuntimeError) as err: + psyclone.process(config, "x90file", api=None, alg_file="alg_file") + assert ("PSyclone called without api, but alg_file is specified" + in str(err.value)) + with pytest.raises(RuntimeError) as err: + psyclone.process(config, "x90file", api=None) + assert ("PSyclone called without api, but transformed_file is not " + "specified" in str(err.value)) @pytest.mark.parametrize("api", ["dynamo0.3", "lfric"]) -def test_psyclone_process_api_2_4_0(api): - '''Test running PSyclone.''' +def test_psyclone_processing_errors_with_api(api): + '''Test all processing errors in PSyclone if an API is specified.''' + psyclone = Psyclone() - mock_result = get_mock_result("2.4.0") + psyclone._is_available = True + config = mock.Mock() + + # No API --> we need transformed file, but not psy or alg: + with pytest.raises(RuntimeError) as err: + psyclone.process(config, "x90file", api=api, psy_file="psy_file") + assert (f"PSyclone called with api '{api}', but no alg_file is specified" + in str(err.value)) + with pytest.raises(RuntimeError) as err: + psyclone.process(config, "x90file", api=api, alg_file="alg_file") + assert (f"PSyclone called with api '{api}', but no psy_file is specified" + in str(err.value)) + with pytest.raises(RuntimeError) as err: + psyclone.process(config, "x90file", api=api, + psy_file="psy_file", alg_file="alg_file", + transformed_file="transformed_file") + assert (f"PSyclone called with api '{api}' and transformed_file" + in str(err.value)) + + +@pytest.mark.parametrize("version", ["2.4.0", "2.5.0"]) +@pytest.mark.parametrize("api", [("dynamo0.3", "dynamo0.3"), + ("lfric", "dynamo0.3"), + ("gocean1.0", "gocean1.0"), + ("gocean", "gocean1.0") + ]) +def test_psyclone_process_api_old_psyclone(api, version): + '''Test running 'old style' PSyclone (2.5.0 and earlier) with the old API + names (dynamo0.3 and gocean1.0). Also check that the new API names will + be accepted, but are mapped to the old style names. The 'api' parameter + contains the input api, and expected output API. + ''' + api_in, api_out = api + psyclone = Psyclone() + mock_result = get_mock_result(version) transformation_function = mock.Mock(return_value="script_called") config = mock.Mock() with mock.patch('fab.tools.tool.subprocess.run', return_value=mock_result) as tool_run: psyclone.process(config=config, - api=api, + api=api_in, x90_file="x90_file", psy_file="psy_file", alg_file="alg_file", @@ -140,16 +204,20 @@ def test_psyclone_process_api_2_4_0(api): kernel_roots=["root1", "root2"], additional_parameters=["-c", "psyclone.cfg"]) tool_run.assert_called_with( - ['psyclone', '-api', 'dynamo0.3', '-opsy', 'psy_file', + ['psyclone', '-api', api_out, '-opsy', 'psy_file', '-oalg', 'alg_file', '-l', 'all', '-s', 'script_called', '-c', 'psyclone.cfg', '-d', 'root1', '-d', 'root2', 'x90_file'], capture_output=True, env=None, cwd=None, check=False) -def test_psyclone_process_no_api_2_4_0(): - '''Test running PSyclone.''' +@pytest.mark.parametrize("version", ["2.4.0", "2.5.0"]) +def test_psyclone_process_no_api_old_psyclone(version): + '''Test running old-style PSyclone (2.5.0 and earlier) when requesting + to transform existing files by not specifying an API. We need to add + the flags `-api nemo` in this case for older PSyclone versions. + ''' psyclone = Psyclone() - mock_result = get_mock_result("2.4.0") + mock_result = get_mock_result(version) transformation_function = mock.Mock(return_value="script_called") config = mock.Mock() @@ -169,6 +237,119 @@ def test_psyclone_process_no_api_2_4_0(): capture_output=True, env=None, cwd=None, check=False) +@pytest.mark.parametrize("version", ["2.4.0", "2.5.0"]) +def test_psyclone_process_nemo_api_old_psyclone(version): + '''Test running old-style PSyclone (2.5.0 and earlier) when requesting + to transform existing files by specifying the nemo api. + ''' + + psyclone = Psyclone() + mock_result = get_mock_result(version) + transformation_function = mock.Mock(return_value="script_called") + config = mock.Mock() + + with mock.patch('fab.tools.tool.subprocess.run', + return_value=mock_result) as tool_run: + psyclone.process(config=config, + api="nemo", + x90_file="x90_file", + transformed_file="psy_file", + transformation_script=transformation_function, + kernel_roots=["root1", "root2"], + additional_parameters=["-c", "psyclone.cfg"]) + tool_run.assert_called_with( + ['psyclone', '-api', 'nemo', '-opsy', 'psy_file', '-l', 'all', + '-s', 'script_called', '-c', + 'psyclone.cfg', '-d', 'root1', '-d', 'root2', 'x90_file'], + capture_output=True, env=None, cwd=None, check=False) + + +@pytest.mark.parametrize("api", [("dynamo0.3", "lfric"), + ("lfric", "lfric"), + ("gocean1.0", "gocean"), + ("gocean", "gocean") + ]) +def test_psyclone_process_api_new__psyclone(api): + '''Test running the new PSyclone version. Since this version is not + yet released, we use the Fab internal version number 2.5.0.1 for + now. It uses new API names, and we need to check that the old style + names are converted to the new names. + ''' + api_in, api_out = api + psyclone = Psyclone() + mock_result = get_mock_result("2.5.0.1") + transformation_function = mock.Mock(return_value="script_called") + config = mock.Mock() + with mock.patch('fab.tools.tool.subprocess.run', + return_value=mock_result) as tool_run: + psyclone.process(config=config, + api=api_in, + x90_file="x90_file", + psy_file="psy_file", + alg_file="alg_file", + transformation_script=transformation_function, + kernel_roots=["root1", "root2"], + additional_parameters=["-c", "psyclone.cfg"]) + tool_run.assert_called_with( + ['psyclone', '--psykal-dsl', api_out, '-opsy', 'psy_file', + '-oalg', 'alg_file', '-l', 'all', '-s', 'script_called', '-c', + 'psyclone.cfg', '-d', 'root1', '-d', 'root2', 'x90_file'], + capture_output=True, env=None, cwd=None, check=False) + + +def test_psyclone_process_no_api_new_psyclone(): + '''Test running the new PSyclone version without an API. Since this + version is not yet released, we use the Fab internal version number + 2.5.0.1 for now. + ''' + psyclone = Psyclone() + mock_result = get_mock_result("2.5.0.1") + transformation_function = mock.Mock(return_value="script_called") + config = mock.Mock() + + with mock.patch('fab.tools.tool.subprocess.run', + return_value=mock_result) as tool_run: + psyclone.process(config=config, + api="", + x90_file="x90_file", + transformed_file="psy_file", + transformation_script=transformation_function, + kernel_roots=["root1", "root2"], + additional_parameters=["-c", "psyclone.cfg"]) + tool_run.assert_called_with( + ['psyclone', '-o', 'psy_file', '-l', 'all', + '-s', 'script_called', '-c', + 'psyclone.cfg', '-d', 'root1', '-d', 'root2', 'x90_file'], + capture_output=True, env=None, cwd=None, check=False) + + +def test_psyclone_process_nemo_api_new_psyclone(): + '''Test running PSyclone. Since this version is not yet released, we use + the Fab internal version number 2.5.0.1 for now. This tests that + backwards compatibility of using the nemo api works, i.e. '-api nemo' is + just removed. + ''' + psyclone = Psyclone() + mock_result = get_mock_result("2.5.0.1") + transformation_function = mock.Mock(return_value="script_called") + config = mock.Mock() + + with mock.patch('fab.tools.tool.subprocess.run', + return_value=mock_result) as tool_run: + psyclone.process(config=config, + api="nemo", + x90_file="x90_file", + transformed_file="psy_file", + transformation_script=transformation_function, + kernel_roots=["root1", "root2"], + additional_parameters=["-c", "psyclone.cfg"]) + tool_run.assert_called_with( + ['psyclone', '-o', 'psy_file', '-l', 'all', + '-s', 'script_called', '-c', + 'psyclone.cfg', '-d', 'root1', '-d', 'root2', 'x90_file'], + capture_output=True, env=None, cwd=None, check=False) + + def test_type_checking_import(): '''PSyclone contains an import of TYPE_CHECKING to break a circular dependency. In order to reach 100% coverage of PSyclone, we set @@ -178,5 +359,6 @@ def test_type_checking_import(): with mock.patch('typing.TYPE_CHECKING', True): # This import will not actually re-import, since the module # is already imported. But we need this in order to call reload: + # pylint: disable=import-outside-toplevel import fab.tools.psyclone reload(fab.tools.psyclone)