Skip to content

Commit 1f3e980

Browse files
james-bruten-mocameronbateman-moyaswant
authored
Validate rose meta (#105)
* add validate rose-meta * styling * debug comment * fix error * Update lfric_macros/validate_rose_meta.py Co-authored-by: Cameron Bateman <cameron.bateman@metoffice.gov.uk> * run black * review comments * Update lfric_macros/validate_rose_meta.py Co-authored-by: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> * Update lfric_macros/validate_rose_meta.py Co-authored-by: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> * fix logic for args * further fix * change order * should be right * move error check --------- Co-authored-by: Cameron Bateman <cameron.bateman@metoffice.gov.uk> Co-authored-by: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com>
1 parent 1c9fec5 commit 1f3e980

1 file changed

Lines changed: 239 additions & 0 deletions

File tree

lfric_macros/validate_rose_meta.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
#!/usr/bin/env python3
2+
##############################################################################
3+
# (c) Crown copyright 2024 Met Office. All rights reserved.
4+
# The file LICENCE, distributed with this code, contains details of the terms
5+
# under which the code may be used.
6+
##############################################################################
7+
"""
8+
Validate the rose metadata in lfric_apps
9+
"""
10+
11+
import os
12+
import sys
13+
import subprocess
14+
import argparse
15+
16+
# A list of invalid metadata sections. Most are invalid as they are imported by
17+
# lfric-gungho but also use the files namelist contained there, creating a circular
18+
# dependency
19+
INVALID_METADATA = [
20+
"jules-lfric",
21+
"lfric-jules-shared",
22+
"socrates-radiation",
23+
"um-aerosol",
24+
"um-boundary_layer",
25+
"um-chemistry",
26+
"um-cloud",
27+
"um-convection",
28+
"um-iau",
29+
"um-microphysics",
30+
"um-orographic_drag",
31+
"um-spectral_gwd",
32+
"um-stochastic_physics",
33+
]
34+
35+
# A list of invalid rose-stem apps to ignore when checking metadata validity. These are
36+
# mostly apps required for lfric coupled which use metadata from other repositories
37+
INVALID_APPS = [
38+
"fcm_make_drivers",
39+
"fcm_make_ocean",
40+
"fcm_make_river",
41+
"lfric_coupled_rivers",
42+
]
43+
44+
45+
def run_command(command, shell=False, env=None):
46+
"""
47+
Run a subprocess command and return the result object
48+
Inputs:
49+
- command, str with command to run
50+
Outputs:
51+
- result object from subprocess.run
52+
"""
53+
if not shell:
54+
command = command.split()
55+
if not env:
56+
env = os.environ
57+
return subprocess.run(
58+
command,
59+
capture_output=True,
60+
text=True,
61+
timeout=120,
62+
shell=shell,
63+
check=False,
64+
env=env,
65+
)
66+
67+
68+
def check_rose_metadata(rose_meta_path, source_path):
69+
"""
70+
Auto find rose-meta sections from the top level rose-meta directory and run `rose
71+
metadata-check` on each
72+
"""
73+
74+
print("\n\n[INFO] - Checking rose metadata sections\n\n")
75+
failed = False
76+
77+
# Add ROSE_META_PATH to env for rose metadata-check command
78+
my_env = os.environ.copy()
79+
my_env["ROSE_META_PATH"] = rose_meta_path
80+
81+
start_dir = os.path.join(source_path, "rose-meta")
82+
dirs = os.listdir(start_dir)
83+
for section in dirs:
84+
if section in INVALID_METADATA:
85+
continue
86+
meta_dir = os.path.join(start_dir, section, "HEAD")
87+
command = f"rose metadata-check --verbose -C {meta_dir}"
88+
result = run_command(command, env=my_env)
89+
if result.returncode:
90+
print(f"[FAIL] - {section} failed to validate")
91+
print(
92+
f"Failure running rose metadata-check on {section}:\n{result.stderr}",
93+
file=sys.stderr,
94+
)
95+
failed = True
96+
else:
97+
print(f"[PASS] - {section} validated")
98+
99+
return failed
100+
101+
102+
def parse_suite_controlled(err_msg):
103+
"""
104+
Remove any app validation error messages resulting from suite_controlled option
105+
configs
106+
"""
107+
108+
err = []
109+
split_err = err_msg.split("\n")
110+
i = 0
111+
while i < len(split_err):
112+
line = split_err[i]
113+
if "opts=suite_controlled" in line:
114+
i += 2
115+
continue
116+
if line.strip():
117+
err.append(line)
118+
i += 1
119+
# There is always a single line in the error stating the number of failures.
120+
# So only return a non-empty list if there is more than just that line
121+
if len(err) > 1:
122+
return err
123+
return []
124+
125+
126+
def check_rose_stem_apps(meta_paths, source_path):
127+
"""
128+
Auto find rose-stem apps that use rose metadata and validate these using 'rose
129+
macro --validate'
130+
"""
131+
132+
print("\n\n[INFO] - Validating rose-stem apps\n\n")
133+
failed = False
134+
135+
start_dir = os.path.join(source_path, "rose-stem", "app")
136+
apps = os.listdir(start_dir)
137+
for app in apps:
138+
if app in INVALID_APPS:
139+
continue
140+
app_dir = os.path.join(start_dir, app)
141+
conf_file = os.path.join(app_dir, "rose-app.conf")
142+
with open(conf_file, "r") as f:
143+
for line in f:
144+
if line.startswith("meta="):
145+
break
146+
else:
147+
# We reach here if the for loop hasn't been broken out of. In that case
148+
# we don't want to execute the code below.
149+
continue
150+
command = f"rose macro --validate {meta_paths} -C {app_dir} --no-warn version"
151+
result = run_command(command)
152+
# Check that errors are only from rose-app-suite-controlled.conf
153+
errors = []
154+
if result.returncode:
155+
errors = parse_suite_controlled(result.stderr)
156+
errors = "\n".join(e for e in errors)
157+
158+
if errors:
159+
print(f"[FAIL] - {app} failed to validate")
160+
print(f"Failure validating app {app}:\n{errors}", file=sys.stderr)
161+
failed = True
162+
else:
163+
print(f"[PASS] - {app} validated")
164+
165+
return failed
166+
167+
168+
def parse_args():
169+
"""
170+
Read command line args
171+
"""
172+
173+
parser = argparse.ArgumentParser(
174+
"Validate rose-metadata in LFRic Meta files and apps"
175+
)
176+
parser.add_argument(
177+
"-a",
178+
"--apps",
179+
default=None,
180+
help="The path to the LFRic Apps source. At least one of this or Core are "
181+
"required. If both are provided then it will be assumed that Apps is the "
182+
"repository to check.",
183+
)
184+
parser.add_argument(
185+
"-c",
186+
"--core",
187+
default=None,
188+
help="The path to the LFRic Core source. At least one of this or Apps are "
189+
"required. If both are provided then it will be assumed that Apps is the "
190+
"repository to check.",
191+
)
192+
193+
args = parser.parse_args()
194+
195+
if not args.apps and not args.core:
196+
raise RuntimeError(
197+
"At least one of the Apps or Core sources must be provided as a command "
198+
"line argument"
199+
)
200+
201+
if args.apps:
202+
args.apps = os.path.expanduser(args.apps)
203+
if args.core:
204+
args.core = os.path.expanduser(args.core)
205+
206+
return args
207+
208+
209+
def main():
210+
"""
211+
main function for this script
212+
"""
213+
args = parse_args()
214+
215+
meta_paths = ""
216+
rose_meta_path = ""
217+
if args.apps:
218+
source_path = args.apps
219+
meta_paths += f"-M {os.path.join(args.apps, "rose-meta")} "
220+
rose_meta_path += args.apps
221+
if args.core:
222+
meta_paths += f"-M {os.path.join(args.core, "rose-meta")} "
223+
if rose_meta_path:
224+
# Apps has already started this
225+
rose_meta_path += f":{args.core}"
226+
else:
227+
# Apps hasn't been set
228+
source_path = args.core
229+
rose_meta_path = args.core
230+
231+
if check_rose_metadata(rose_meta_path, source_path) or check_rose_stem_apps(
232+
meta_paths, source_path
233+
):
234+
sys.exit("There were metadata validation failures. See output for details")
235+
print("All metadata successfully validated")
236+
237+
238+
if __name__ == "__main__":
239+
main()

0 commit comments

Comments
 (0)