Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6a5c65e
initial commit for suite_report
james-bruten-mo Sep 1, 2025
0b4a2d7
task tables done
james-bruten-mo Sep 1, 2025
982a94f
update '
james-bruten-mo Sep 1, 2025
00a6b16
make executable
james-bruten-mo Sep 1, 2025
a3f4fec
update suite_report
james-bruten-mo Sep 1, 2025
3aa2a1f
run black
james-bruten-mo Sep 1, 2025
2b76c1f
revert test_fcm_bdiff deletion
james-bruten-mo Sep 1, 2025
b35d280
add finally with error to import statement
james-bruten-mo Sep 1, 2025
485f7e3
didn't want finally
james-bruten-mo Sep 1, 2025
889ab47
revert bdiff changes
james-bruten-mo Sep 2, 2025
83a99d4
add check for writable suite_path
james-bruten-mo Sep 2, 2025
3cd0ef7
update for jules primary source
james-bruten-mo Sep 2, 2025
9959ede
add class docstring
james-bruten-mo Sep 2, 2025
a8e1bca
Merge branch 'main' into suite_report_git
james-bruten-mo Sep 3, 2025
30f4af1
edit dependency table
james-bruten-mo Sep 3, 2025
38710b1
run black
james-bruten-mo Sep 3, 2025
0db65f4
Update suite_report_git/suite_data.py
james-bruten-mo Sep 4, 2025
25e7edc
Update suite_report_git/suite_data.py
james-bruten-mo Sep 4, 2025
95353ee
Update suite_report_git/suite_data.py
james-bruten-mo Sep 4, 2025
7150b0f
change to using urls
james-bruten-mo Sep 4, 2025
608d4a0
Merge branch 'suite_report_git' of github.com:james-bruten-mo/SimSys_…
james-bruten-mo Sep 4, 2025
67b343a
url for local clones
james-bruten-mo Sep 5, 2025
673a425
run black
james-bruten-mo Sep 5, 2025
f8b89e3
collapse failed
james-bruten-mo Sep 5, 2025
70e3e62
Merge branch 'main' into suite_report_git
james-bruten-mo Sep 9, 2025
b1ca3bc
code review comments
james-bruten-mo Sep 15, 2025
5c63484
Merge branch 'suite_report_git' of github.com:james-bruten-mo/SimSys_…
james-bruten-mo Sep 15, 2025
9b08418
run black
james-bruten-mo Sep 15, 2025
608634a
Apply suggestion from @t00sa
james-bruten-mo Sep 16, 2025
757f65f
black
james-bruten-mo Sep 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
315 changes: 315 additions & 0 deletions suite_report_git/suite_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
#!/usr/bin/env python3
# *****************************COPYRIGHT*******************************
# (C) Crown copyright Met Office. All rights reserved.
# For further details please refer to the file COPYRIGHT.txt
# which you should have received as part of this distribution.
# *****************************COPYRIGHT*******************************
"""
Class containing helper methods for gathering data needed for a SuiteReport object
"""

import sys

sys.path.append("../")
import subprocess
import sqlite3
import shutil
import yaml
import re

try:
from bdiff.git_bdiff import GitBDiff, GitInfo
except ImportError:
try:
from git_bdiff import GitBDiff, GitInfo
except ImportError:
raise ImportError(
"Unable to import from git_bdiff module. This is included in the same "
"repository as this script and included with a relative import. Ensure "
"this script is being called from the correct place."
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
except ImportError:
raise ImportError(
"Unable to import from git_bdiff module. This is included in the same "
"repository as this script and included with a relative import. Ensure "
"this script is being called from the correct place."
)
except ImportError as err:
raise ImportError(
"Unable to import from git_bdiff module. This is included in the same "
"repository as this script and included with a relative import. Ensure "
"this script is being called from the correct place."
) from err

from typing import Union, Optional, List, Dict
from pathlib import Path
from collections import defaultdict


class SuiteData:
"""
Class to gather info on a suite
"""

pink_failures = (
"_vs_",
"lrun_crun_atmos",
"proc",
"atmos_omp",
"atmos_nruncrun",
"atmos_thread",
"-v-",
)

Comment thread
james-bruten-mo marked this conversation as resolved.
def parse_tasks(self) -> Dict[str, List[str]]:
"""
Read through the tasks run, sorting by state
"""

data = defaultdict(list)

for task, state in self.task_states.items():
if state == "failed" and (
task.startswith("rose_ana") or task.startswith("check_")
):
for item in self.pink_failures:
if item in task:
state = "pink failure"
break
data[state].append(task)
return data

def populate_gitbdiff(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Needs a return type?

"""
Run GitBDiff on each copied source if the source isn't main-like, storing in the
dependencies directory
"""

for dependency, data in self.dependencies.items():
if not data["gitinfo"].is_main():
if dependency.lower() == "simsys_scripts":
parent = "main"
else:
parent = "trunk"
self.dependencies[dependency]["gitbdiff"] = GitBDiff(
repo=self.temp_directory / dependency, parent=parent
).files()

def populate_gitinfo(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Needs a return type?

"""
Run GitInfo on each copied source, storing in the dependencies directory
"""

for dependency in self.dependencies:
self.dependencies[dependency]["gitinfo"] = GitInfo(
self.temp_directory / dependency
)

def clone_sources(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Needs a return type?

"""
Clone the sources defined in the dependencies file, to allow reading of files
and creation of diffs.
If the source is not a github url, then copy it using rsync
"""

for dependency, data in self.dependencies.items():
loc = self.temp_directory / dependency
if data["source"].endswith(".git"):
commands = [
f"git clone {data['source']} {loc}",
f"git -C {loc} checkout {data['ref']}",
]
for command in commands:
self.run_command(command)
else:
source = data["source"]
if not source.endswith("/"):
source = source + "/"
command = (
f'rsync -e "ssh -o StrictHostKeyChecking=no" -avl {source} {loc}'
)
self.run_command(command, shell=True)

def determine_primary_source(self) -> str:
"""
Work out what repo launched the suite based on the what sources are available in
the dependencies. Return 'unknown' if unable to determine.
"""

# If just 1 dependency, it must be that
if len(self.dependencies) == 1:
return list(self.dependencies.keys())[0]

# If 2 dependencies, remove simsys_scripts
if len(self.dependencies) == 2:
for item in self.dependencies:
if item.lower() != "simsys_scripts":
return item

# If LFRic Apps in sources, that is the primary source
if "lfric_apps" in self.dependencies.keys():
return "lfric_apps"

if "um" in self.dependencies.keys():
return "um"

return "unknown"

def read_rose_conf(self) -> Dict[str, str]:
"""
Read the suite rose-suite.conf file into a dictionary
"""

path = self.suite_path / "log" / "config"
for item in path.rglob("*-rose-suite.conf"):
conf_file = item
break

Comment thread
t00sa marked this conversation as resolved.
rose_conf_text = conf_file.read_text().split("\n")
rose_conf = {}
for line in rose_conf_text:
line = line.strip()
if (
not line
or line.startswith("!")
or line.startswith("[")
or line.startswith("#")
Comment thread
t00sa marked this conversation as resolved.
):
continue
line = line.split("=")
key = line[0].strip()
value = line[1].strip()
if (
value.startswith("'")
and value.endswith("'")
or value.startswith('"')
and value.endswith('"')
):
value = value[1:-1]
rose_conf[key] = value
return rose_conf

def find_unknown_dependency(self, dependency: str) -> str:
"""
TEMPORARY
The primary dependency may be unset in the dependencies file. In this case find
it from the *_SOURCE variable in the rose-suite.conf.
TODO: Once cylc provides the location of the source code itself, this method
should be changed to use that instead, as then the _SOURCE variable will be
removed
"""

var = f"{dependency.upper()}_SOURCE".replace('"', "")
if var not in self.rose_data:
raise RuntimeError(f"Cant determine source for {dependency}")
rval = self.rose_data[var]
if "$ROSE_ORIG_HOST" in rval:
rval = rval.replace("$ROSE_ORIG_HOST", self.rose_data["ROSE_ORIG_HOST"])
return rval

def read_dependencies(self) -> Dict[str, Dict]:
"""
Read the suite dependencies from the dependencies.yaml file - this is assumed to
have been copied to the suite_path directory
"""

with open(self.suite_path / "dependencies.yaml", "r") as stream:
dependencies = yaml.safe_load(stream)
for dependecy, data in dependencies.items():
Comment thread
james-bruten-mo marked this conversation as resolved.
Outdated
if data["source"] is None:
dependencies[dependecy]["source"] = self.find_unknown_dependency(
dependecy
Comment thread
james-bruten-mo marked this conversation as resolved.
Outdated
)
return dependencies

def get_workflow_id(self) -> str:
Comment thread
t00sa marked this conversation as resolved.
"""
Read cylc install log for workflow id
"""

with open(self.suite_path / "log" / "scheduler" / "log", "r") as f:
for line in f:
match = re.search(r"INFO - Workflow: (\w+\/\w+)", line)
try:
workflow_id = match.group(1)
return workflow_id
except IndexError:
continue

def get_suite_starttime(self) -> str:
"""
Read the suite starttime from the suite database
"""

starttime = "unknown"
for row in self.query_suite_database(
self.suite_path / "log" / "db", ["start_time"], "workflow_flows"
):
starttime = row[0]
return starttime.split("+")[0]

def read_groups_run(self) -> str:
"""
Read in groups run as part of suite from the cylc database file
"""

groups = None
for row in self.query_suite_database(
self.suite_path / "log" / "db", ["key", "value"], "workflow_template_vars"
):
if row[0] in ("g", "groups"):
groups = row[1].strip("[]'\"").split(",")
break
return groups

def get_task_states(self) -> Dict[str, str]:
"""
Query the database and return a dictionary of states. This is assumed to be in
suite_path/log/db
"""

data = {}
for row in self.query_suite_database(
self.suite_path / "log" / "db", ["name", "status"], "task_states"
):
data[row[0]] = row[1]
return data

def query_suite_database(
self, database: Path, selections: List[str], source: str
) -> List[tuple]:
"""
Create an sql statement annd query provided database. Return the result
Comment thread
james-bruten-mo marked this conversation as resolved.
Outdated
"""

sql_statement = f"select {', '.join(s for s in selections)} from {source};"

database = sqlite3.connect(database)
cursor = database.cursor()
cursor.execute(sql_statement)
data = []
for row in cursor:
data.append(row)
database.close()
return data

def run_command(
self, command: Union[str, List[str]], shell: bool = False, rval: bool = False
) -> Optional[subprocess.CompletedProcess]:
"""
Run a subprocess command and return the result object
Inputs:
- command, str with command to run
Outputs:
- result object from subprocess.run
"""
if not shell and type(command) != list:
command = command.split()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is probably better as:

Suggested change
if not shell and type(command) != list:
command = command.split()
if not shell and isinstance(command, str):
command = command.split()

result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=300,
shell=shell,
check=False,
)
if result.returncode:
print(result.stdout, end="\n\n\n")
raise RuntimeError(
f"[FAIL] Issue found running command {command}\n\n{result.stderr}"
)
if rval:
return result

def cleanup(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Needs a return type?

"""
Remove self.temp_directory
"""
shutil.rmtree(self.temp_directory)
Loading