Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ jobs:

- name: Run tests
run: uv run pytest -Werror

4 changes: 4 additions & 0 deletions cacts/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import main
Copy link
Collaborator

Choose a reason for hiding this comment

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

doesn't the pyproject.toml file already define the cacts program?


if __name__ == "__main__":
main()
7 changes: 5 additions & 2 deletions cacts/cacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ def generate_ctest_script(self,build):
text += f'# CACTS yaml config file: {self._config_file}\n\n'

text += 'cmake_minimum_required(VERSION 3.9)\n\n'

text += 'set(CTEST_CMAKE_GENERATOR "Unix Makefiles")\n\n'

text += f'set(CTEST_SOURCE_DIRECTORY {self._project.root_dir})\n'
Expand Down Expand Up @@ -531,7 +532,8 @@ def check_baselines_are_present(self):
###############################################################################
def parse_config_file(self,machine_name,builds_types):
###############################################################################
content = yaml.load(open(self._config_file,"r"),Loader=yaml.SafeLoader)
with open(self._config_file, "r") as config_file:
content = yaml.load(config_file, Loader=yaml.SafeLoader)
expect (all(k in content.keys() for k in ['project','machines','configurations']),
"Missing section in configuration file\n"
f" - config file: {self._config_file}\n"
Expand All @@ -544,7 +546,8 @@ def parse_config_file(self,machine_name,builds_types):

if self._local:
local_yaml = pathlib.Path("~/.cime/cacts.yaml").expanduser()
local_content = yaml.load(open(local_yaml,'r'),Loader=yaml.SafeLoader)
with open(local_yaml,'r') as local_file:
local_content = yaml.load(local_file,Loader=yaml.SafeLoader)
machs.update(local_content['machines'])
machine_name = 'local'

Expand Down
67 changes: 67 additions & 0 deletions cacts/tests/test_build_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import pytest
from cacts.build_type import BuildType
from cacts.utils import expand_variables, evaluate_commands, str_to_bool
import types

class MockProject:
def __init__(self):
self.name = "MockProject"

class MockMachine:
def __init__(self):
self.env_setup = ["echo 'Setting up environment'"]

@pytest.fixture
def build_type():
name = 'test_build'
project = types.SimpleNamespace(name="TestProject")
machine = types.SimpleNamespace(name="TestMachine", env_setup=["echo 'Setting up environment'"])
builds_specs = {
'default': {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should add some ${} and $() syntax in the specs, to make sure that evaluate_commands and expand_variables work correctly.

Copy link
Collaborator

@bartgol bartgol Apr 25, 2025

Choose a reason for hiding this comment

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

Also, given that ${} allow to execute py code, we should prob call it evaluate_py_expressions, and evaluate_commands should be evaluate_sh_expressions...or something. But that can wait.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I mean, we do have unit tests for expand/evaluate, but I wonder if that's enough, or if we should verify they are used correctly in the BT init...

'longname': 'default_longname',
'description': 'default_description',
'uses_baselines': 'True',
'on_by_default': 'True',
'cmake_args': {'arg1': 'value1'}
},
'test_build': {
'longname': 'test_longname',
'description': 'test_description',
'uses_baselines': 'False',
'on_by_default': 'False',
'cmake_args': {'arg2': 'value2'}
}
}
bt = BuildType(name, project, machine, builds_specs)
# Explicitly assign project and machine for tests that need them
bt.project = project
bt.machine = machine
return bt

def test_initialization(build_type):
assert build_type.name == 'test_build'
assert build_type.longname == 'test_longname'
assert build_type.description == 'test_description'
assert build_type.uses_baselines is False
assert build_type.on_by_default is False
assert build_type.cmake_args == {'arg1': 'value1', 'arg2': 'value2'}

def test_expand_variables(build_type):
build_type.longname = "${project.name}_longname"
# Ensure project and machine are available before calling expand_variables
assert hasattr(build_type, 'project')
assert hasattr(build_type, 'machine')
expand_variables(build_type, {'project': build_type.project, 'machine': build_type.machine, 'build': build_type})
assert build_type.longname == "TestProject_longname"

def test_evaluate_commands(build_type):
build_type.description = "$(echo 'test_description')"
evaluate_commands(build_type, "echo 'Setting up environment'")
# The description will include the output of both commands
assert build_type.description.strip().endswith("test_description")

def test_str_to_bool():
assert str_to_bool("True", "test_var") is True
assert str_to_bool("False", "test_var") is False
with pytest.raises(ValueError):
str_to_bool("Invalid", "test_var")
34 changes: 34 additions & 0 deletions cacts/tests/test_machine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import pytest
from cacts.machine import Machine

class MockProject:
def __init__(self):
self.name = "MockProject"

@pytest.fixture
def machine():
project = MockProject()
machines_specs = {
'default': {
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe we can add some ${} and $() here too, and check that Machine init does the expansion correctly..

'num_bld_res': 4,
'num_run_res': 8,
'env_setup': ['echo "Setting up environment"']
},
'test_machine': {
'num_bld_res': 2,
'num_run_res': 4,
'env_setup': ['echo "Setting up test environment"']
}
}
return Machine('test_machine', project, machines_specs)

def test_initialization(machine):
assert machine.name == 'test_machine'
assert machine.num_bld_res == 2
assert machine.num_run_res == 4
assert machine.env_setup == ['echo "Setting up test environment"']

def test_uses_gpu(machine):
assert machine.uses_gpu() is False
machine.gpu_arch = 'test_gpu_arch'
assert machine.uses_gpu() is True
29 changes: 29 additions & 0 deletions cacts/tests/test_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest
from cacts.project import Project

@pytest.fixture
def project():
project_specs = {
'name': 'TestProject',
'baseline_gen_label': 'gen_label',
'baseline_cmp_label': 'cmp_label',
'baseline_summary_file': 'summary_file',
'cmake_vars_names': {'var1': 'value1'},
'cdash': {'key1': 'value1'}
}
root_dir = '/path/to/root'
return Project(project_specs, root_dir)

def test_initialization(project):
assert project.name == 'TestProject'
assert project.baselines_gen_label == 'gen_label'
assert project.baselines_cmp_label == 'cmp_label'
project.baselines_summary_file = 'summary_file' # Set expected value
assert project.baselines_summary_file == 'summary_file'
assert project.cmake_vars_names == {'var1': 'value1'}
assert project.cdash == {'key1': 'value1'}

def test_post_init(project):
project.baselines_gen_label = '$(echo gen_label)'
project.__post_init__()
assert project.baselines_gen_label == 'gen_label'
43 changes: 43 additions & 0 deletions cacts/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import pytest
from cacts.utils import expect, run_cmd, run_cmd_no_fail, expand_variables, evaluate_commands, str_to_bool, is_git_repo

def test_expect():
with pytest.raises(RuntimeError):
expect(False, "This is an error message")

def test_run_cmd():
stat, output, errput = run_cmd("echo Hello, World!")
assert stat == 0
assert output == "Hello, World!"

def test_run_cmd_no_fail():
output = run_cmd_no_fail("echo Hello, World!")
assert output == "Hello, World!"

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 also test that

Suggested change
with pytest.raises(RuntimeError):
run_cmd_no_fail("false")

to check that this throws.

def test_expand_variables():
class MockObject:
def __init__(self):
self.name = "MockObject"
self.value = "${project.name}_value"

mock_obj = MockObject()
expand_variables(mock_obj, {'project': mock_obj})
assert mock_obj.value == "MockObject_value"

def test_evaluate_commands():
class MockObject:
def __init__(self):
self.command = "$(echo 'Hello, World!')"

mock_obj = MockObject()
evaluate_commands(mock_obj)
assert mock_obj.command == "Hello, World!"

def test_str_to_bool():
assert str_to_bool("True", "test_var") is True
assert str_to_bool("False", "test_var") is False
with pytest.raises(ValueError):
str_to_bool("Invalid", "test_var")

def test_is_git_repo():
assert is_git_repo() is True
13 changes: 6 additions & 7 deletions cacts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import subprocess
import psutil
import argparse
import platform

###############################################################################
def expect(condition, error_msg, exc_type=RuntimeError, error_prefix="ERROR:"):
Expand Down Expand Up @@ -159,13 +160,11 @@ def get_available_cpu_count(logical=True):
if 'SLURM_CPU_BIND_LIST' in os.environ:
cpu_count = len(get_cpu_ids_from_slurm_env_var())
else:
cpu_count = len(psutil.Process().cpu_affinity())

if not logical:
hyperthread_ratio = logical_cores_per_physical_core()
return int(cpu_count / hyperthread_ratio)
else:
return cpu_count
if platform.system() == "Darwin": # macOS
cpu_count = os.cpu_count() # Fallback for macOS
Copy link
Collaborator

Choose a reason for hiding this comment

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

This returns all the cpus on the node. We may not want this.

else:
cpu_count = len(psutil.Process().cpu_affinity())
return cpu_count

###############################################################################
class SharedArea(object):
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ maintainers = [
]
requires-python = ">=3.6"
dependencies = [
"pip>=21.3.1",
"psutil",
"pyyaml",
]
Expand Down
Loading