Skip to content

Commit c1c9a3d

Browse files
authored
Merge pull request #3238 from samsrabin/python-test-portability
Add GitHub workflow for Python unit tests
2 parents 1f80940 + bd695ac commit c1c9a3d

File tree

9 files changed

+209
-146
lines changed

9 files changed

+209
-146
lines changed

.github/workflows/formatting_python.yml renamed to .github/workflows/python-tests.yml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Check Python formatting
1+
name: Run Python tests
22

33
on:
44
push:
@@ -18,7 +18,27 @@ on:
1818
- 'cime_config/buildnml/**'
1919

2020
jobs:
21-
lint-and-format-check:
21+
python-unit-tests:
22+
runs-on: ubuntu-latest
23+
steps:
24+
# Checkout the code
25+
- uses: actions/checkout@v4
26+
27+
# Set up the conda environment
28+
- uses: conda-incubator/setup-miniconda@v3
29+
with:
30+
activate-environment: ctsm_pylib
31+
environment-file: python/conda_env_ctsm_py.yml
32+
channels: conda-forge
33+
auto-activate-base: false
34+
35+
# Run Python unit tests check
36+
- name: Run Python unit tests
37+
run: |
38+
cd python
39+
conda run -n ctsm_pylib ./run_ctsm_py_tests -u
40+
41+
python-lint-and-black:
2242
runs-on: ubuntu-latest
2343
steps:
2444
# Checkout the code

cime_config/SystemTests/sspmatrixcn.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""
1515

1616
import shutil, glob, os, sys
17+
from pathlib import Path
1718
from datetime import datetime
1819

1920
if __name__ == "__main__":
@@ -205,9 +206,9 @@ def run_indv(self, nstep, st_archive=True):
205206
restdir = os.path.join(rest_r, rundate)
206207
os.mkdir(restdir)
207208
rpoint = os.path.join(restdir, "rpointer.clm." + rundate)
208-
os.mknod(rpoint)
209+
Path.touch(rpoint)
209210
rpoint = os.path.join(restdir, "rpointer.cpl." + rundate)
210-
os.mknod(rpoint)
211+
Path.touch(rpoint)
211212

212213
def run_phase(self):
213214
"Run phase"

python/ctsm/subset_data.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ def determine_num_pft(crop):
605605
return num_pft
606606

607607

608-
def setup_files(args, defaults, cesmroot):
608+
def setup_files(args, defaults, cesmroot, testing=False):
609609
"""
610610
Sets up the files and folders needed for this program
611611
"""
@@ -623,9 +623,9 @@ def setup_files(args, defaults, cesmroot):
623623
else:
624624
clmforcingindir = args.inputdatadir
625625

626-
if not os.path.isdir(clmforcingindir):
626+
if not testing and not os.path.isdir(clmforcingindir):
627627
logger.info("clmforcingindir does not exist: %s", clmforcingindir)
628-
abort("inputdata directory does not exist")
628+
abort(f"inputdata directory does not exist: {clmforcingindir}")
629629

630630
file_dict = {"main_dir": clmforcingindir}
631631

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
System tests for gen_mksurfdata_jobscript_single.py subroutines on Derecho
5+
"""
6+
7+
import unittest
8+
import os
9+
10+
from ctsm import unit_testing
11+
from ctsm.test_gen_mksurfdata_jobscript_single_parent import TestFGenMkSurfJobscriptSingleParent
12+
from ctsm.path_utils import path_to_cime
13+
from ctsm.os_utils import run_cmd_output_on_error
14+
from ctsm.toolchain.gen_mksurfdata_jobscript_single import get_parser
15+
from ctsm.toolchain.gen_mksurfdata_jobscript_single import get_mpirun
16+
from ctsm.toolchain.gen_mksurfdata_jobscript_single import check_parser_args
17+
from ctsm.toolchain.gen_mksurfdata_jobscript_single import write_runscript_part1
18+
19+
20+
# Allow test names that pylint doesn't like; otherwise hard to make them
21+
# readable
22+
# pylint: disable=invalid-name
23+
24+
25+
# pylint: disable=protected-access
26+
# pylint: disable=too-many-instance-attributes
27+
class TestFGenMkSurfJobscriptSingleDerecho(TestFGenMkSurfJobscriptSingleParent):
28+
"""Tests the gen_mksurfdata_jobscript_single subroutines on Derecho"""
29+
30+
def test_derecho_mpirun(self):
31+
"""
32+
test derecho mpirun. This would've helped caught a problem we ran into
33+
It will also be helpful when sumodules are updated to guide to solutions
34+
to problems
35+
"""
36+
machine = "derecho"
37+
nodes = 4
38+
tasks = 128
39+
unit_testing.add_machine_node_args(machine, nodes, tasks)
40+
args = get_parser().parse_args()
41+
check_parser_args(args)
42+
self.assertEqual(machine, args.machine)
43+
self.assertEqual(tasks, args.tasks_per_node)
44+
self.assertEqual(nodes, args.number_of_nodes)
45+
self.assertEqual(self._account, args.account)
46+
# Create the env_mach_specific.xml file needed for get_mpirun
47+
# This will catch problems with our usage of CIME objects
48+
# Doing this here will also catch potential issues in the gen_mksurfdata_build script
49+
configure_path = os.path.join(path_to_cime(), "CIME", "scripts", "configure")
50+
self.assertTrue(os.path.exists(configure_path))
51+
options = " --macros-format CMake --silent --compiler intel --machine " + machine
52+
cmd = configure_path + options
53+
cmd_list = cmd.split()
54+
run_cmd_output_on_error(
55+
cmd=cmd_list, errmsg="Trouble running configure", cwd=self._bld_path
56+
)
57+
self.assertTrue(os.path.exists(self._env_mach))
58+
expected_attribs = {"mpilib": "default"}
59+
with open(self._jobscript_file, "w", encoding="utf-8") as runfile:
60+
attribs = write_runscript_part1(
61+
number_of_nodes=nodes,
62+
tasks_per_node=tasks,
63+
machine=machine,
64+
account=self._account,
65+
walltime=args.walltime,
66+
runfile=runfile,
67+
)
68+
self.assertEqual(attribs, expected_attribs)
69+
(executable, mksurfdata_path, env_mach_path) = get_mpirun(args, attribs)
70+
expected_exe = "time mpibind "
71+
self.assertEqual(executable, expected_exe)
72+
self.assertEqual(mksurfdata_path, self._mksurf_exe)
73+
self.assertEqual(env_mach_path, self._env_mach)
74+
75+
76+
if __name__ == "__main__":
77+
unit_testing.setup_for_tests()
78+
unittest.main()

python/ctsm/test/test_unit_gen_mksurfdata_jobscript_single.py

Lines changed: 9 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -6,106 +6,25 @@
66

77
import unittest
88
import os
9-
import sys
109
import shutil
1110

12-
import tempfile
13-
1411
from ctsm import unit_testing
15-
from ctsm.path_utils import path_to_ctsm_root
16-
from ctsm.path_utils import path_to_cime
17-
from ctsm.os_utils import run_cmd_output_on_error
12+
from ctsm.test_gen_mksurfdata_jobscript_single_parent import TestFGenMkSurfJobscriptSingleParent
1813
from ctsm.toolchain.gen_mksurfdata_jobscript_single import get_parser
19-
from ctsm.toolchain.gen_mksurfdata_jobscript_single import get_mpirun
2014
from ctsm.toolchain.gen_mksurfdata_jobscript_single import check_parser_args
2115
from ctsm.toolchain.gen_mksurfdata_jobscript_single import write_runscript_part1
2216

2317

24-
def add_args(machine, nodes, tasks):
25-
"""add arguments to sys.argv"""
26-
args_to_add = [
27-
"--machine",
28-
machine,
29-
"--number-of-nodes",
30-
str(nodes),
31-
"--tasks-per-node",
32-
str(tasks),
33-
]
34-
for item in args_to_add:
35-
sys.argv.append(item)
36-
37-
38-
def create_empty_file(filename):
39-
"""create an empty file"""
40-
os.system("touch " + filename)
41-
42-
4318
# Allow test names that pylint doesn't like; otherwise hard to make them
4419
# readable
4520
# pylint: disable=invalid-name
4621

4722

4823
# pylint: disable=protected-access
4924
# pylint: disable=too-many-instance-attributes
50-
class TestFGenMkSurfJobscriptSingle(unittest.TestCase):
25+
class TestFGenMkSurfJobscriptSingle(TestFGenMkSurfJobscriptSingleParent):
5126
"""Tests the gen_mksurfdata_jobscript_single subroutines"""
5227

53-
def setUp(self):
54-
"""Setup for trying out the methods"""
55-
testinputs_path = os.path.join(path_to_ctsm_root(), "python/ctsm/test/testinputs")
56-
self._testinputs_path = testinputs_path
57-
self._previous_dir = os.getcwd()
58-
self._tempdir = tempfile.mkdtemp()
59-
os.chdir(self._tempdir)
60-
self._account = "ACCOUNT_NUMBER"
61-
self._jobscript_file = "output_jobscript"
62-
self._output_compare = """#!/bin/bash
63-
# Edit the batch directives for your batch system
64-
# Below are default batch directives for derecho
65-
#PBS -N mksurfdata
66-
#PBS -j oe
67-
#PBS -k eod
68-
#PBS -S /bin/bash
69-
#PBS -l walltime=12:00:00
70-
#PBS -A ACCOUNT_NUMBER
71-
#PBS -q main
72-
#PBS -l select=1:ncpus=128:mpiprocs=64:mem=218GB
73-
74-
# This is a batch script to run a set of resolutions for mksurfdata_esmf input namelist
75-
# NOTE: THIS SCRIPT IS AUTOMATICALLY GENERATED SO IN GENERAL YOU SHOULD NOT EDIT it!!
76-
77-
"""
78-
self._bld_path = os.path.join(self._tempdir, "tools_bld")
79-
os.makedirs(self._bld_path)
80-
self.assertTrue(os.path.isdir(self._bld_path))
81-
self._nlfile = os.path.join(self._tempdir, "namelist_file")
82-
create_empty_file(self._nlfile)
83-
self.assertTrue(os.path.exists(self._nlfile))
84-
self._mksurf_exe = os.path.join(self._bld_path, "mksurfdata")
85-
create_empty_file(self._mksurf_exe)
86-
self.assertTrue(os.path.exists(self._mksurf_exe))
87-
self._env_mach = os.path.join(self._bld_path, ".env_mach_specific.sh")
88-
create_empty_file(self._env_mach)
89-
self.assertTrue(os.path.exists(self._env_mach))
90-
sys.argv = [
91-
"gen_mksurfdata_jobscript_single",
92-
"--bld-path",
93-
self._bld_path,
94-
"--namelist-file",
95-
self._nlfile,
96-
"--jobscript-file",
97-
self._jobscript_file,
98-
"--account",
99-
self._account,
100-
]
101-
102-
def tearDown(self):
103-
"""
104-
Remove temporary directory
105-
"""
106-
os.chdir(self._previous_dir)
107-
shutil.rmtree(self._tempdir, ignore_errors=True)
108-
10928
def assertFileContentsEqual(self, expected, filepath, msg=None):
11029
"""Asserts that the contents of the file given by 'filepath' are equal to
11130
the string given by 'expected'. 'msg' gives an optional message to be
@@ -123,7 +42,7 @@ def test_simple_derecho_args(self):
12342
machine = "derecho"
12443
nodes = 1
12544
tasks = 64
126-
add_args(machine, nodes, tasks)
45+
unit_testing.add_machine_node_args(machine, nodes, tasks)
12746
args = get_parser().parse_args()
12847
check_parser_args(args)
12948
with open(self._jobscript_file, "w", encoding="utf-8") as runfile:
@@ -139,57 +58,12 @@ def test_simple_derecho_args(self):
13958

14059
self.assertFileContentsEqual(self._output_compare, self._jobscript_file)
14160

142-
def test_derecho_mpirun(self):
143-
"""
144-
test derecho mpirun. This would've helped caught a problem we ran into
145-
It will also be helpful when sumodules are updated to guide to solutions
146-
to problems
147-
"""
148-
machine = "derecho"
149-
nodes = 4
150-
tasks = 128
151-
add_args(machine, nodes, tasks)
152-
args = get_parser().parse_args()
153-
check_parser_args(args)
154-
self.assertEqual(machine, args.machine)
155-
self.assertEqual(tasks, args.tasks_per_node)
156-
self.assertEqual(nodes, args.number_of_nodes)
157-
self.assertEqual(self._account, args.account)
158-
# Create the env_mach_specific.xml file needed for get_mpirun
159-
# This will catch problems with our usage of CIME objects
160-
# Doing this here will also catch potential issues in the gen_mksurfdata_build script
161-
configure_path = os.path.join(path_to_cime(), "CIME", "scripts", "configure")
162-
self.assertTrue(os.path.exists(configure_path))
163-
options = " --macros-format CMake --silent --compiler intel --machine " + machine
164-
cmd = configure_path + options
165-
cmd_list = cmd.split()
166-
run_cmd_output_on_error(
167-
cmd=cmd_list, errmsg="Trouble running configure", cwd=self._bld_path
168-
)
169-
self.assertTrue(os.path.exists(self._env_mach))
170-
expected_attribs = {"mpilib": "default"}
171-
with open(self._jobscript_file, "w", encoding="utf-8") as runfile:
172-
attribs = write_runscript_part1(
173-
number_of_nodes=nodes,
174-
tasks_per_node=tasks,
175-
machine=machine,
176-
account=self._account,
177-
walltime=args.walltime,
178-
runfile=runfile,
179-
)
180-
self.assertEqual(attribs, expected_attribs)
181-
(executable, mksurfdata_path, env_mach_path) = get_mpirun(args, attribs)
182-
expected_exe = "time mpibind "
183-
self.assertEqual(executable, expected_exe)
184-
self.assertEqual(mksurfdata_path, self._mksurf_exe)
185-
self.assertEqual(env_mach_path, self._env_mach)
186-
18761
def test_too_many_tasks(self):
18862
"""test trying to use too many tasks"""
18963
machine = "derecho"
19064
nodes = 1
19165
tasks = 129
192-
add_args(machine, nodes, tasks)
66+
unit_testing.add_machine_node_args(machine, nodes, tasks)
19367
args = get_parser().parse_args()
19468
check_parser_args(args)
19569
with open(self._jobscript_file, "w", encoding="utf-8") as runfile:
@@ -212,7 +86,7 @@ def test_zero_tasks(self):
21286
machine = "derecho"
21387
nodes = 5
21488
tasks = 0
215-
add_args(machine, nodes, tasks)
89+
unit_testing.add_machine_node_args(machine, nodes, tasks)
21690
args = get_parser().parse_args()
21791
with self.assertRaisesRegex(
21892
SystemExit,
@@ -225,7 +99,7 @@ def test_bld_build_path(self):
22599
machine = "derecho"
226100
nodes = 10
227101
tasks = 64
228-
add_args(machine, nodes, tasks)
102+
unit_testing.add_machine_node_args(machine, nodes, tasks)
229103
# Remove the build path directory
230104
shutil.rmtree(self._bld_path, ignore_errors=True)
231105
args = get_parser().parse_args()
@@ -237,7 +111,7 @@ def test_mksurfdata_exist(self):
237111
machine = "derecho"
238112
nodes = 10
239113
tasks = 64
240-
add_args(machine, nodes, tasks)
114+
unit_testing.add_machine_node_args(machine, nodes, tasks)
241115
args = get_parser().parse_args()
242116
os.remove(self._mksurf_exe)
243117
with self.assertRaisesRegex(SystemExit, "mksurfdata_esmf executable "):
@@ -248,7 +122,7 @@ def test_env_mach_specific_exist(self):
248122
machine = "derecho"
249123
nodes = 10
250124
tasks = 64
251-
add_args(machine, nodes, tasks)
125+
unit_testing.add_machine_node_args(machine, nodes, tasks)
252126
args = get_parser().parse_args()
253127
os.remove(self._env_mach)
254128
with self.assertRaisesRegex(SystemExit, "Environment machine specific file"):
@@ -259,7 +133,7 @@ def test_bad_machine(self):
259133
machine = "zztop"
260134
nodes = 1
261135
tasks = 64
262-
add_args(machine, nodes, tasks)
136+
unit_testing.add_machine_node_args(machine, nodes, tasks)
263137
with self.assertRaises(SystemExit):
264138
get_parser().parse_args()
265139

python/ctsm/test/test_unit_sspmatrix.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def create_clone(
5353
Extend to handle creation of user_nl_clm file
5454
"""
5555
clone = super().create_clone(newcase, keepexe=keepexe)
56-
os.mknod(os.path.join(newcase, "user_nl_clm"))
56+
Path.touch(os.path.join(newcase, "user_nl_clm"))
5757
# Also make the needed case directories
5858
clone.make_case_dirs(self._tempdir)
5959
return clone
@@ -165,7 +165,7 @@ def test_append_user_nl_step2(self):
165165
if os.path.exists(ufile):
166166
os.remove(ufile)
167167

168-
os.mknod(ufile)
168+
Path.touch(ufile)
169169

170170
expect = "\nhist_nhtfrq = -8760, hist_mfilt = 2\n"
171171
self.ssp.append_user_nl(caseroot=".", n=2)

0 commit comments

Comments
 (0)