# Copyright 2022-2025 The Ramble Authors
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.
import argparse
import collections
import io
import re
import sys
import llnl.util.tty.color as color
from llnl.util.filesystem import working_dir
from llnl.util.tty.colify import colify
import ramble.paths
import ramble.workspace
from ramble.util.logger import logger
description = "run ramble's unit tests (wrapper around pytest)"
section = "developer"
level = "long"
[docs]
def setup_parser(subparser):
subparser.add_argument(
"-H",
"--pytest-help",
action="store_true",
default=False,
help="show full pytest help, with advanced options",
)
# extra ramble arguments to list tests
list_group = subparser.add_argument_group("listing tests")
list_mutex = list_group.add_mutually_exclusive_group()
list_mutex.add_argument(
"-l",
"--list",
action="store_const",
default=None,
dest="list",
const="list",
help="list test filenames",
)
list_mutex.add_argument(
"-L",
"--list-long",
action="store_const",
default=None,
dest="list",
const="long",
help="list all test functions",
)
list_mutex.add_argument(
"-N",
"--list-names",
action="store_const",
default=None,
dest="list",
const="names",
help="list full names of all tests",
)
# spell out some common pytest arguments, so they'll show up in help
pytest_group = subparser.add_argument_group(
"common pytest arguments (ramble unit-test --pytest-help for more)"
)
pytest_group.add_argument(
"-s",
action="append_const",
dest="parsed_args",
const="-s",
help="print output while tests run (disable capture)",
)
pytest_group.add_argument(
"-k",
action="store",
metavar="EXPRESSION",
dest="expression",
help="filter tests by keyword (can also use w/list options)",
)
pytest_group.add_argument(
"--showlocals",
action="append_const",
dest="parsed_args",
const="--showlocals",
help="show local variable values in tracebacks",
)
# remainder is just passed to pytest
subparser.add_argument("pytest_args", nargs=argparse.REMAINDER, help="arguments for pytest")
[docs]
def do_list(args, extra_args):
"""Print a lists of tests than what pytest offers."""
# Run test collection and get the tree out.
old_output = sys.stdout
try:
sys.stdout = output = io.StringIO()
try:
import pytest
pytest.main(["--collect-only"] + extra_args)
except ImportError:
logger.die("Pytest python module not found. Ensure requirements.txt are installed.")
finally:
sys.stdout = old_output
lines = output.getvalue().split("\n")
tests = collections.defaultdict(set)
prefix = []
print("All lines =")
print(lines)
# collect tests into sections
for line in lines:
match = re.match(r"(\s*)<([^ ]*) '([^']*)'", line)
if not match:
continue
indent, nodetype, name = match.groups()
# strip parametrized tests
if "[" in name:
name = name[: name.index("[")]
depth = len(indent) // 2
if nodetype.endswith("Function"):
key = tuple(prefix)
tests[key].add(name)
print(f"added test {key}={name} from {nodetype}")
else:
prefix = prefix[:depth]
prefix.append(name)
print(f"added prefix {name}")
def colorize(c, prefix):
if isinstance(prefix, tuple):
return "::".join(color.colorize(f"@{c}{{{p}}}") for p in prefix if p != "()")
return color.colorize(f"@{c}{{{prefix}}}")
if args.list == "list":
files = {prefix[0] for prefix in tests}
color_files = [colorize("B", file) for file in sorted(files)]
colify(color_files)
elif args.list == "long":
for prefix, functions in sorted(tests.items()):
path = colorize("*B", prefix) + "::"
functions = [colorize("c", f) for f in sorted(functions)]
color.cprint(path)
colify(functions, indent=4)
print()
else: # args.list == "names"
all_functions = [
colorize("*B", prefix) + "::" + colorize("c", f)
for prefix, functions in sorted(tests.items())
for f in sorted(functions)
]
colify(all_functions)
[docs]
def add_back_pytest_args(args, unknown_args):
"""Add parsed pytest args, unknown args, and remainder together.
We add some basic pytest arguments to the Ramble parser to ensure that
they show up in the short help, so we have to reassemble things here.
"""
result = args.parsed_args or []
result += unknown_args or []
result += args.pytest_args or []
if args.expression:
result += ["-k", args.expression]
return result
[docs]
def unit_test(parser, args, unknown_args):
if args.pytest_help:
# make the pytest.main help output more accurate
sys.argv[0] = "ramble test"
try:
import pytest
return pytest.main(["-h"])
except ImportError:
logger.die("Pytest python module not found. Ensure requirements.txt are installed.")
# add back any parsed pytest args we need to pass to pytest
pytest_args = add_back_pytest_args(args, unknown_args)
pytest_root = ramble.paths.ramble_root
# pytest.ini lives in the root of the ramble repository.
with working_dir(pytest_root):
if args.list:
do_list(args, pytest_args)
return
with ramble.workspace.no_active_workspace():
try:
import pytest
return pytest.main(pytest_args)
except ImportError:
logger.die(
"Pytest python module not found. Ensure requirements.txt are installed."
)