Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt subprocess fixture for testing. #391

Open
wants to merge 23 commits into
base: develop
Choose a base branch
from

Conversation

MatthewHambley
Copy link
Collaborator

Rather than variations of the same hand-mocking of shelling out to a tool this change takes advantage of an existing fixture.

It also pulls in a fixture to create a fake filing system in memory which should be faster and less prone to faults based on what the developer has in their file tree.

@MatthewHambley MatthewHambley marked this pull request as ready for review March 4, 2025 11:58
@MatthewHambley MatthewHambley requested review from a team and MGEX82 and removed request for a team March 4, 2025 11:58
@yaswant yaswant requested a review from hiker March 4, 2025 14:04
@MatthewHambley MatthewHambley removed the request for review from MGEX82 March 4, 2025 14:17
Copy link
Collaborator

@hiker hiker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only got through parts of it, will try to continue either later today or tomorrow.

@@ -82,7 +82,7 @@ def add(self, collection: Union[str, ArtefactSet],
self[collection].update(files)

def update_dict(self, collection: Union[str, ArtefactSet],
key: str, values: Union[str, Iterable]):
key: Optional[str], values: Union[str, Iterable]):
'''For ArtefactSets that are a dictionary of sets: update
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You know that better than me: it appears that only one test supplies a 'None' as key. If None is otherwise not expected, I would suggest to change the test instead of allowing None here.

@@ -98,7 +98,7 @@ def from_dict(cls, d):
return result


def extract_sub_tree(source_tree: Dict[Path, AnalysedDependent], root: Path, verbose=False)\
def extract_sub_tree(source_tree: Mapping[Path, AnalysedDependent], root: Path, verbose=False)\
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for my own education: what is the advantage of using Mapping? I thought mapping is more generic than Dict, so if we only expect dict, shouldn't we use dict?

@@ -79,7 +79,8 @@ def run_mp_imap(config, items, func, result_handler):
result_handler(analysis_results)


def check_for_errors(results, caller_label=None):
def check_for_errors(results: Iterable[Union[str, Exception]],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding more type hints!

@@ -56,6 +56,7 @@ def __init__(self, name: str,
compile_flag: Optional[str] = None,
output_flag: Optional[str] = None,
openmp_flag: Optional[str] = None,
version_argument: Optional[str] = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been thinking that I should have added this - thanks!

self._version_regex = version_regex

@property
def mpi(self) -> bool:
''':returns: whether this compiler supports MPI or not.'''
'''Returns whether this compiler supports MPI or not.'''
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you remove the ::? Using :returns: means that if we ever automatically create a reference guide using sphinx or so, the return value will be properly documented
(see e.g. https://psyclone-ref.readthedocs.io/en/latest/autogenerated/psyclone.core.signature.html#psyclone.core.signature.Signature.is_structure)

@@ -82,7 +84,7 @@ def openmp(self) -> bool:

@property
def openmp_flag(self) -> str:
''':returns: the flag to enable OpenMP.'''
'''Returns the flag to enable OpenMP.'''
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@@ -163,15 +162,15 @@ def compile_file(self, input_file: Path,
# which also supports the syntax_only flag anyway)
self._compiler = cast(FortranCompiler, self._compiler)
self._compiler.compile_file(input_file, output_file, openmp=openmp,
add_flags=self.flags + add_flags,
add_flags=self._flags + add_flags,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer using the implemented property, not the private attribute. While this has a tiny bit of overhead, the advantage is that this can be overwritten by a derived class. Now, I find overwriting a property bad tbh, and I believe I fix this properly in the compiler profile support PR, where I turns this into a real function get_flags() (iirc)

syntax_only=syntax_only,
)
else:
if syntax_only is not None:
raise RuntimeError(f"Syntax-only cannot be used with compiler "
f"'{self.name}'.")
self._compiler.compile_file(input_file, output_file, openmp=openmp,
add_flags=self.flags+add_flags
add_flags=self._flags+add_flags
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto - I would prefer this to stay, so I have less conflicts to resolve later ;)

@@ -182,7 +182,7 @@ def run(self,
res = subprocess.run(command, capture_output=capture_output,
env=env, cwd=cwd, check=False)
except FileNotFoundError as err:
raise RuntimeError(f"Command '{command}' could not be "
raise RuntimeError(f"Command '{' '.join(command)}' could not be "
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should stay with the original version. Since our subprocess does not use a shell, that means that a space in a parameter will potentially result in errors (e.g. ["-l /some_lib"] will not be recognised by the linker). While I actually prefer the new look, and it would make it easier for other develoeprs, this error message will then totally hide what the problem is, which will lead to frustrating debugging.
Alternatively, I would be happy to use a shell in subprocess, but you didn't want to do that.

@@ -75,7 +75,7 @@ def __init__(self):
# Add the common shells. While Fab itself does not need this,
# it is a very convenient tool for user configuration (e.g. to
# query nc-config etc)
for shell_name in ["sh", "bash", "ksh", "dash"]:
for shell_name in ["sh", "bash", "ksh", "dash", 'zsh']:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I am totally confused now. I am just preparing the shell PR (that you have already reviewed) to go into main, and one comment from you was to only support sh. So I have modified this branch before submitting it to only support sh, but you are adding more shells. What do you prefer? I am happy to go either way

Copy link
Collaborator

@hiker hiker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still more to come, sorry

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that file required?

from fab.tools.tool_box import ToolBox


def not_found_callback(process):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add some comments to all these functions.

psyclone_tool = Psyclone()
psyclone_tool._version = (2, 4, 0)
psyclone_tool._is_available = True

mock_transformation_script = mock.Mock(return_value=__file__)
with mock.patch('fab.tools.psyclone.Psyclone.run') as mock_run_command:
mock_transformation_script.return_value = Path(__file__)
psyclone_tool.process(api=psyclone_lfric_api,
psyclone_tool.process(api='dynamo0.3',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I am ok with removing the psyclone_lfric_api definintion (which allowed them all to be changed at one location), you then need to switch to lfric, not dynamo0.3, which is deprecated.

@@ -219,7 +227,7 @@ def test_transformation_script(self, psyclone_lfric_api):
mock_transformation_script.assert_called_once_with(Path(__file__), None)
# check transformation_script is passed to psyclone command with '-s'
mock_run_command.assert_called_with(
additional_parameters=['-api', psyclone_lfric_api,
additional_parameters=['-api', 'dynamo0.3',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lfric

mp_common_args = Mock(config=config)
with raises(RuntimeError) as err:
_compile_file((Mock(), mp_common_args))
assert str(err.value) == "Unexpected tool 'some C compiler' of category " \
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pep8 (https://peps.python.org/pep-0008/#maximum-line-length):
"Long lines can be broken over multiple lines by wrapping expressions in parentheses. These should be used in preference to using a backslash for line continuation."

"""
config, _ = content

monkeypatch.setenv('CFLAGS', '-Denv_flag')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I didn't know that - that's neat :)

kwargs = {'project_label': 'fortran explicit gfortran', 'fab_workspace': tmp_path, 'multiprocessing': False}
ToDo: Fragility due to assumption of source donor.
"""
pytest.importorskip('clang', reason="Missing libclang bindings.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Copy link
Collaborator

@hiker hiker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I am stopping here and pass it back to you (extra long weekend) - main reason: could you explain to me the advantage of using fs? I have the feeling I am missing something :) So basically, it is a temporary directory, that is just automatically cleared at the end of a test. In my (PSyclone-heavy ;) ) experience, I often use the files in tmp to debug failing tests. So if files are instead created in tmp, I can't debug them to see e.g. what might be wrong. We also have a fixture for changing into tmpdir, which seems to be the equivalent of fs, except in case of failures you can analyse the files. I would also prefer slightly smaller PRs ;) I am aware that this is already a split of an even larger one, but I am sure I missed a fs fixture a couple of times before wondering what that might be ;) I'll pass it back to you at least for clarification of fs, and pick it up next week.

Tests the archive step.
"""
def test_for_exes(self, stub_tool_box: ToolBox,
fs, fake_process: FakeProcess) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not your fault of course - but that is a HORRIBLE name. Took me forever to find out what on earth fs might even be. Luckily there was one test that typed it :)

@hiker
Copy link
Collaborator

hiker commented Mar 7, 2025

Oh, also bringing it up to recent develop would be great - the psyclone changes in master are causing the conflict ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants