Improve unit testing#99
Conversation
f8a064d to
8f80a70
Compare
Currently, benchcab does not have extensive coverage in its unit tests. Parts of the code that have poor test coverage are dependent on system interaction (e.g. environment modules, running subprocess commands (SVN, build scripts, executables, PBS). This change refactors and adds unit testing for these parts of the code base. Move the code from the benchcab.run_cable_site module to the benchcab.task module. This is done so that we reduce coupling between modules that use fluxnet tasks. This also has the benefit that we can test the two modules easily by sharing the same test setup functions. We move the code from the benchcab.run_comparison module to benchcab.task module for the same reasons. Unit tests now check exception messages and standard output produced by benchcab for both non-verbose and verbose modes. **Beware:** standard output messages have been updated in some areas. These changes will need to be updated in the documentation. Refactor the default build so that environment modules are loaded via python instead of writing module load statements into the temporary build script. This is done to reduce the complexity of the unit tests. The stderr output from subprocess commands are now redirected to stdout so that stderr is suppressed in non-verbose mode. Fixes #58 #22 #86
8f80a70 to
e2813ac
Compare
88f2575 to
0e016ee
Compare
0e016ee to
865da7a
Compare
73e04f0 to
5eb457c
Compare
This change refactors the code to be more object oriented so that we can better support mocking via dependency injection rather than resorting to the `unittest.mock.patch` function. This allows us to write unit tests that are simpler and that preserve the API layer (as opposed to using `unittest.mock.patch` which breaks the API layer). Fixes #102
5eb457c to
ecc7940
Compare
Codecov Report
@@ Coverage Diff @@
## master #99 +/- ##
===========================================
+ Coverage 66.29% 88.26% +21.97%
===========================================
Files 20 26 +6
Lines 1062 1364 +302
===========================================
+ Hits 704 1204 +500
+ Misses 358 160 -198
|
ccarouge
left a comment
There was a problem hiding this comment.
There are a few things but everything should be straight forward. Feel free to refuse on anything I've put in.
| """Returns a PBS job that executes all computationally expensive commands. | ||
|
|
||
| This includes things such as running CABLE and running bitwise comparison jobs | ||
| between model output files. The PBS job script is written to the current | ||
| working directory as a side effect. | ||
| """ |
There was a problem hiding this comment.
| """Returns a PBS job that executes all computationally expensive commands. | |
| This includes things such as running CABLE and running bitwise comparison jobs | |
| between model output files. The PBS job script is written to the current | |
| working directory as a side effect. | |
| """ | |
| """Returns the text for a PBS job script that executes all computationally expensive commands. | |
| This includes things such as running CABLE and running bitwise comparison jobs | |
| between model output files. | |
| """ |
| stdout_file = bitwise_cmp_dir / "mock_comparison_task_name.txt" | ||
|
|
||
| # Failure case: test failed comparison check (files differ) | ||
| mock_subprocess = MockSubprocessWrapper() | ||
| mock_subprocess.error_on_call = True | ||
| task = get_mock_comparison_task(subprocess_handler=mock_subprocess) |
There was a problem hiding this comment.
| stdout_file = bitwise_cmp_dir / "mock_comparison_task_name.txt" | |
| # Failure case: test failed comparison check (files differ) | |
| mock_subprocess = MockSubprocessWrapper() | |
| mock_subprocess.error_on_call = True | |
| task = get_mock_comparison_task(subprocess_handler=mock_subprocess) | |
| # Failure case: test failed comparison check (files differ) | |
| mock_subprocess = MockSubprocessWrapper() | |
| mock_subprocess.error_on_call = True | |
| task = get_mock_comparison_task(subprocess_handler=mock_subprocess) | |
| stdout_file = bitwise_cmp_dir / f"{task.task_name}.txt" | |
Simply moves the def. of stdout_file after creating the task and re-use task_name. Makes it easier to understand where the name of the stdout_file comes from and avoids breaking the test by changing get_mock_comparison_task().
| task = get_mock_comparison_task(subprocess_handler=mock_subprocess) | ||
| task.run() | ||
| with open(stdout_file, "r", encoding="utf-8") as file: | ||
| assert file.read() == "mock standard output" |
There was a problem hiding this comment.
| assert file.read() == "mock standard output" | |
| assert file.read() == mock_subprocess.stdout |
| verbose=verbose, | ||
| ) | ||
|
|
||
| def custom_build(self, verbose=False) -> None: |
There was a problem hiding this comment.
I have moved the issue to work on merging this with the default build to the top of the list. This way we don't have to worry about this method here.
| mock_subprocess = MockSubprocessWrapper() | ||
| mock_subprocess.stdout = "mock standard output" | ||
| repo = get_mock_repo(mock_subprocess) | ||
| assert repo.svn_info_show_item("some-mock-item") == "mock standard output" |
There was a problem hiding this comment.
| assert repo.svn_info_show_item("some-mock-item") == "mock standard output" | |
| assert repo.svn_info_show_item("some-mock-item") == mock_subprocess.stdout |
| project=self.config["project"], modules=self.config["modules"] | ||
| ) | ||
|
|
||
| def _validate_environment(self, project: str, modules: list): |
There was a problem hiding this comment.
We could write tests for this. pytest allows to mark tests and choose to run only some. So we could check all the failures on GitHub (which are the most interesting tests actually) and add a "Gadi-only" test for success that we disable from the automatic testing on GitHub.
I am happy to leave it for another pull request.
There was a problem hiding this comment.
Yep that is possible. Although I would prefer if we didn't have Gadi specific tests (this wouldn't count towards code coverage). I actually think it would be straight forward to refactor _validate_environment so that it is testable using dependency injection. But we can probably save this for later 😄
| if not self.modules_handler.module_is_loaded("nccmp"): | ||
| self.modules_handler.module_load( | ||
| "nccmp" | ||
| ) # use `nccmp -df` for bitwise comparisons |
There was a problem hiding this comment.
| if not self.modules_handler.module_is_loaded("nccmp"): | |
| self.modules_handler.module_load( | |
| "nccmp" | |
| ) # use `nccmp -df` for bitwise comparisons | |
| if not self.modules_handler.module_is_loaded("nccmp/1.8.5.0"): | |
| self.modules_handler.module_load( | |
| "nccmp/1.8.5.0" | |
| ) # use `nccmp -df` for bitwise comparisons |
Always specify the version of the module when loading a module. Other versions can be installed and default versions can change with time.
| assert buf.getvalue() == ( | ||
| "Creating PBS job script to run FLUXNET tasks on compute " | ||
| f"nodes: {internal.QSUB_FNAME}\n" | ||
| "Error when submitting job to NCI queue\nmock standard output\n" |
There was a problem hiding this comment.
| "Error when submitting job to NCI queue\nmock standard output\n" | |
| f"Error when submitting job to NCI queue\n{mock_subprocess.stdout}\n" |
| patch_namelist(nml_path, {"cable": {"file": "/path/to/file", "bar": 123}}) | ||
| assert f90nml.read(nml_path) == { | ||
| "cable": { | ||
| "file": "/path/to/file", | ||
| "bar": 123, | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Can you put the patch dictionary in a variable and reuse that variable in the assert? Instead of rewriting the whole dictionary.
| assert atts["cable_branch"] == "mock standard output" | ||
| assert atts["svn_revision_number"] == "mock standard output" |
There was a problem hiding this comment.
| assert atts["cable_branch"] == "mock standard output" | |
| assert atts["svn_revision_number"] == "mock standard output" | |
| assert atts["cable_branch"] == mock_subprocess.stdout | |
| assert atts["svn_revision_number"] == mock_subprocess.stdout |
Co-authored-by: Claire Carouge <claire.carouge@anu.edu.au>
ccarouge
left a comment
There was a problem hiding this comment.
Finally, done. That took more time than expected but I think it was worth it, for benchcab and for us.
Currently, the CABLE executable is run from the current working directory instead of the task directory by using the absolute path to the executable. However, we should be running the executable from the task directory (otherwise CABLE will complain it cannot find the soil and PFT namelist files). This was a bug that was introduced in the #99. This change makes it so we change into the task directory before running the CABLE executable. Fixes #123
Currently, the CABLE executable is run from the current working directory instead of the task directory by using the absolute path to the executable. However, we should be running the executable from the task directory (otherwise CABLE will complain it cannot find the soil and PFT namelist files). This was a bug that was introduced in the #99. This change makes it so we change into the task directory before running the CABLE executable. Fixes #123
Currently,
benchcabdoes not have extensive coverage in its unit tests. Parts of the code that have poor test coverage are dependent on system interaction (e.g. environment modules, running subprocess commands (SVN, build scripts, executables, PBS).This change refactors and adds unit testing for these parts of the code base. The pytest code coverage is now at 89%.
Move the code from the
benchcab.run_cable_sitemodule to thebenchcab.taskmodule. This is done so that we reduce coupling between modules that use fluxnet tasks. This also has the benefit that we can test the two modules easily by sharing the same test setup functions. We move the code from thebenchcab.run_comparisonmodule tobenchcab.taskmodule for the same reasons.Unit tests now check exception messages and standard output produced by
benchcabfor both non-verbose and verbose modes.Beware: standard output messages have been updated in some areas. These changes will need to be updated in the documentation.Changes have now been updated.Refactor the default build so that environment modules are loaded via python instead of writing module load statements into the temporary build script. This is done to reduce the complexity of the unit tests.
The stderr output from subprocess commands are now redirected to stdout so that stderr is suppressed in non-verbose mode.
Fixes #58 #22 #86 #102