Skip to content
Merged
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
9 changes: 8 additions & 1 deletion .github/scripts/build-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,11 @@ cmd="$CMAKE -S /mnt -B . -DCMAKE_CXX_COMPILER=$CXX -DBUILD_GTESTS=ON $CMAKE_EXTR
echo $cmd
$cmd
cmake --build .
cmake --build . --target test

# Run tests, showing the output of any failing test; as the build directory
# is lost when the container exits, also dump the full test log on failure
if ! ctest --output-on-failure; then
echo "===== Testing/Temporary/LastTest.log ====="
cat Testing/Temporary/LastTest.log
exit 1
fi
2 changes: 0 additions & 2 deletions .github/scripts/flake8.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
#! /bin/sh

# First we need to install Python and Flake 8
# N.B. this is for an Alma Linux container (specifically almalinux 9
# but should be quite generic)
dnf -y install python3 python-pip cmake gcc-c++ boost boost-devel make
pip install flake8

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ jobs:
# Define the platforms to run on. Later determines the Docker image to be used
strategy:
matrix:
platform: [ almalinux:8, almalinux:9, almalinux:10, ubuntu:22.04, ubuntu:24.04 ]
platform: [ almalinux:9, almalinux:10, ubuntu:24.04, ubuntu:26.04 ]
compiler: [ gcc ]
include:
# On Alma 9 we also test clang
- platform: almalinux:9
# On Alma 10 we also test clang
- platform: almalinux:10
compiler: clang

# Steps represent a sequence of tasks that will be executed as part of the job
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/flake8.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ jobs:
# Sets up useful environment variables
- name: Setup environment variables
run: |
echo "DIMAGE=almalinux:9" >> $GITHUB_ENV
echo "DIMAGE=almalinux:latest" >> $GITHUB_ENV
env:
PLATFORM: almalinux:9
PLATFORM: almalinux:latest

# Pulls the associated Docker image
- name: Docker pull
Expand Down
3 changes: 2 additions & 1 deletion package/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,11 @@ script_install(SCRIPT test_io.py)
script_install(SCRIPT test_net.py)
script_install(SCRIPT test_mem.py)
script_install(SCRIPT net_burner.py)
script_install(SCRIPT http_block.py DESTINATION cgi-bin)
script_install(SCRIPT http_block.py)
script_install(SCRIPT test_count.py)
script_install(SCRIPT test_exit.py)
script_install(SCRIPT test_disable.py)
script_install(SCRIPT prmon_test_utils.py)

# Setup the target version of Python we will use for testing
set(PYTHON_TEST "python3" CACHE STRING "Python binary to use for tests")
Expand Down
63 changes: 38 additions & 25 deletions package/tests/http_block.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,53 @@
#! /usr/bin/env python3
#
# Copyright (C) 2018-2025 CERN
# Copyright (C) 2018-2026 CERN
# License Apache2 - see LICENCE file

"""Simple CGI script that delivers a known block
of data to the caller over HTTP
"""Simple HTTP server that delivers a known block
of data to the caller

One GET/POST parameter is recognised, which is "blocks"
that specifies how many 1KB blocks are returned to the
client (defaults to 1000, thus ~1MB delivered)"""
One GET parameter is recognised, which is "blocks"
that specifies how many 1KB blocks are returned to the
client (defaults to 1000, thus ~1MB delivered)"""

import cgi
import cgitb
import sys
import argparse
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse

MY_STRING = b"somehow the world seems more curious than when i was a child xx\n"
MY_BLOCK = MY_STRING * 16 # 1KB

def main():
"""main function"""
cgitb.enable()

print("Content-Type: text/plain\n")
class BlockHandler(BaseHTTPRequestHandler):
"""Return the requested number of 1KB blocks for any GET request"""

def do_GET(self): # noqa: N802 (name required by BaseHTTPRequestHandler)
"""Handle a GET request"""
query = parse_qs(urlparse(self.path).query)
try:
blocks = int(query.get("blocks", ["1000"])[0])
except ValueError:
self.send_error(400, "Invalid block value")
return

form = cgi.FieldStorage()
blocks = form.getfirst("blocks", default="1000")
try:
blocks = int(blocks)
except ValueError:
print("Invalid block value")
sys.exit(1)
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(MY_BLOCK) * blocks))
self.end_headers()
for _ in range(blocks):
self.wfile.write(MY_BLOCK)

my_string = "somehow the world seems more curious than when i was a child xx\n"
my_block = my_string * 16 # 1KB

for _ in range(blocks):
print(my_block)
def main():
"""parse arguments and serve until killed"""
parser = argparse.ArgumentParser(description="HTTP data block server")
parser.add_argument("--port", type=int, default=8000)
parser.add_argument("--host", default="localhost")
args = parser.parse_args()

with HTTPServer((args.host, args.port), BlockHandler) as server:
server.serve_forever()


if "__main__" in __name__:
if __name__ == "__main__":
main()
4 changes: 2 additions & 2 deletions package/tests/net_burner.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#! /usr/bin/env python3
#
# Copyright (C) 2018-2025 CERN
# Copyright (C) 2018-2026 CERN
# License Apache2 - see LICENCE file

"""Request network data for prmon unittest"""
Expand All @@ -12,7 +12,7 @@

def get_net_data(host="localhost", port="8000", blocks=None):
"""read network data from local http server"""
url = "http://" + host + ":" + str(port) + "/cgi-bin/http_block.py"
url = "http://" + host + ":" + str(port) + "/"
if blocks:
url += "?blocks=" + str(blocks)
# This URL is fine as it's from the server we setup
Expand Down
38 changes: 38 additions & 0 deletions package/tests/prmon_test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#! /usr/bin/env python3
#
# Copyright (C) 2018-2026 CERN
# License Apache2 - see LICENCE file

"""Common utilities for the prmon python test wrappers"""

import functools
import sys

PRMON_OUTPUT_FILES = ("prmon.json", "prmon.txt", "prmon.log")


def dump_prmon_outputs(files=PRMON_OUTPUT_FILES):
"""Print prmon output files, to help debug failures where the
files are not accessible afterwards (e.g., in CI)"""
for filename in files:
print(f"===== {filename} =====", file=sys.stderr)
try:
with open(filename) as infile:
print(infile.read(), file=sys.stderr)
except OSError as err:
print(f"Could not read {filename}: {err}", file=sys.stderr)


def dump_on_failure(test_method):
"""Decorator for test methods that dumps the prmon output files
if the test fails (or errors), then re-raises the exception"""

@functools.wraps(test_method)
def wrapper(*args, **kwargs):
try:
return test_method(*args, **kwargs)
except Exception:
dump_prmon_outputs()
raise

return wrapper
5 changes: 4 additions & 1 deletion package/tests/test_count.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#! /usr/bin/env python3
#
# Copyright (C) 2018-2025 CERN
# Copyright (C) 2018-2026 CERN
# License Apache2 - see LICENCE file

"""prmon test harness to count number of processes"""
Expand All @@ -11,13 +11,16 @@
import sys
import unittest

from prmon_test_utils import dump_on_failure


def setup_configurable_test(threads=1, procs=1, time=10.0, interval=1, invoke=False):
"""Wrap the class definition in a function to allow arguments to be passed"""

class ConfigurableProcessMonitor(unittest.TestCase):
"""Test class for a specific set of parameters"""

@dump_on_failure
def test_run_test_with_params(self):
"""Actual test runner"""
burn_cmd = ["./burner", "--time", str(time)]
Expand Down
5 changes: 4 additions & 1 deletion package/tests/test_cpu.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#! /usr/bin/env python3
#
# Copyright (C) 2018-2025 CERN
# Copyright (C) 2018-2026 CERN
# License Apache2 - see LICENCE file

"""Test harness for process and thread burning tests"""
Expand All @@ -11,6 +11,8 @@
import sys
import unittest

from prmon_test_utils import dump_on_failure


def setup_configurable_test(
threads=1,
Expand All @@ -27,6 +29,7 @@ def setup_configurable_test(
class ConfigurableProcessMonitor(unittest.TestCase):
"""Test class for a specific set of parameters"""

@dump_on_failure
def test_run_test_with_params(self):
"""Actual test runner"""
burn_cmd = ["./burner", "--time", str(time)]
Expand Down
5 changes: 4 additions & 1 deletion package/tests/test_disable.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#! /usr/bin/env python3
#
# Copyright (C) 2020-2025 CERN
# Copyright (C) 2020-2026 CERN
# License Apache2 - see LICENCE file

"""prmon test harness to check monitors can be disabled correctly"""
Expand All @@ -12,6 +12,8 @@
import sys
import unittest

from prmon_test_utils import dump_on_failure


def setup_configurable_test(disable=[]):
"""Wrap the class definition in a function to allow arguments to be passed"""
Expand All @@ -28,6 +30,7 @@ class ConfigurableProcessMonitor(unittest.TestCase):
"nvidiamon": "ngpus",
}

@dump_on_failure
def setup_and_run(self, envdisable):
"""Baseline test run"""
burn_cmd = ["./burner", "--time", "3"]
Expand Down
5 changes: 4 additions & 1 deletion package/tests/test_exit.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#! /usr/bin/env python3
#
# Copyright (C) 2018-2025 CERN
# Copyright (C) 2018-2026 CERN
# License Apache2 - see LICENCE file

"""prmon test case for monitored process exit code"""
Expand All @@ -10,13 +10,16 @@
import sys
import unittest

from prmon_test_utils import dump_on_failure


def setup_configurable_test(exit_code=0):
"""Wrap the class definition in a function to allow arguments to be passed"""

class ConfigurableProcessMonitor(unittest.TestCase):
"""Wrap the class definition in a function to allow arguments to be passed"""

@dump_on_failure
def test_run_test_with_params(self):
"""Actual test runner"""
child_cmd = ["sh", "-c", "sleep 3 && exit {0}".format(exit_code)]
Expand Down
5 changes: 4 additions & 1 deletion package/tests/test_io.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#! /usr/bin/env python3
#
# Copyright (C) 2018-2025 CERN
# Copyright (C) 2018-2026 CERN
# License Apache2 - see LICENCE file

"""prmon test for measuring i/o"""
Expand All @@ -11,6 +11,8 @@
import sys
import unittest

from prmon_test_utils import dump_on_failure


def setup_configurable_test(
io=10, threads=1, procs=1, usleep=10, pause=1, slack=0.95, interval=1
Expand All @@ -20,6 +22,7 @@ def setup_configurable_test(
class ConfigurableProcessMonitor(unittest.TestCase):
"""Test class for a specific set of parameters"""

@dump_on_failure
def test_run_test_with_params(self):
"""Actual test runner"""
burn_cmd = ["./io-burner", "--io", str(io)]
Expand Down
5 changes: 4 additions & 1 deletion package/tests/test_mem.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#! /usr/bin/env python3
#
# Copyright (C) 2018-2025 CERN
# Copyright (C) 2018-2026 CERN
# License Apache2 - see LICENCE file

"""prmon unitest for memory consumption"""
Expand All @@ -11,6 +11,8 @@
import sys
import unittest

from prmon_test_utils import dump_on_failure


def setup_configurable_test(
procs=4, malloc_mb=100, write_fraction=0.5, sleep=10, slack=0.9, interval=1
Expand All @@ -37,6 +39,7 @@ def check_memory_limits(self, name, value, expected, slack):
f"(expected maximum of {min_value}, got {value})",
)

@dump_on_failure
def test_run_test_with_params(self):
"""Actual test runner"""
burn_cmd = [
Expand Down
9 changes: 6 additions & 3 deletions package/tests/test_net.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#! /usr/bin/env python3
#
# Copyright (C) 2018-2025 CERN
# Copyright (C) 2018-2026 CERN
# License Apache2 - see LICENCE file

"""Test harness script for network IO"""
Expand All @@ -13,6 +13,8 @@
import sys
import unittest

from prmon_test_utils import dump_on_failure

THE_TEST = None


Expand All @@ -25,15 +27,16 @@ class ConfigurableProcessMonitor(unittest.TestCase):
"""Test class for a specific set of parameters"""

def setUp(self):
"""Start the simple python HTTP CGI server"""
http_server_cmd = ["python3", "-m", "http.server", "--cgi"]
"""Start the simple python HTTP data block server"""
http_server_cmd = ["python3", "./http_block.py"]
self.http_server = subprocess.Popen(http_server_cmd, shell=False)

def tearDown(self):
"""Kill http server"""
os.kill(self.http_server.pid, signal.SIGTERM)
self.http_server.wait()

@dump_on_failure
def test_run_test_with_params(self):
"""Test class for a specific set of parameters"""
burn_cmd = ["python3", "./net_burner.py"]
Expand Down
Loading