From 5afd0c3f17b2a0698f6e80e1e126b4e70c235cd3 Mon Sep 17 00:00:00 2001 From: Bryan Van de Ven Date: Thu, 29 Sep 2022 13:50:23 -0700 Subject: [PATCH] Move test driver code to legate.core --- test.py | 8 +- tests/_utils/__init__.py | 74 ----- tests/_utils/args.py | 286 ------------------ tests/_utils/config.py | 161 ---------- tests/_utils/logger.py | 67 ---- tests/_utils/stages/__init__.py | 41 --- tests/_utils/stages/_linux/__init__.py | 24 -- tests/_utils/stages/_linux/cpu.py | 80 ----- tests/_utils/stages/_linux/eager.py | 71 ----- tests/_utils/stages/_linux/gpu.py | 82 ----- tests/_utils/stages/_linux/omp.py | 84 ----- tests/_utils/stages/_osx/__init__.py | 24 -- tests/_utils/stages/_osx/cpu.py | 64 ---- tests/_utils/stages/_osx/eager.py | 64 ---- tests/_utils/stages/_osx/gpu.py | 51 ---- tests/_utils/stages/_osx/omp.py | 70 ----- tests/_utils/stages/test_stage.py | 265 ---------------- tests/_utils/stages/util.py | 115 ------- tests/_utils/system.py | 170 ----------- tests/_utils/test_plan.py | 131 -------- tests/_utils/tests/__init__.py | 15 - tests/_utils/tests/stages/__init__.py | 38 --- tests/_utils/tests/stages/_linux/__init__.py | 22 -- tests/_utils/tests/stages/_linux/test_cpu.py | 131 -------- .../_utils/tests/stages/_linux/test_eager.py | 81 ----- tests/_utils/tests/stages/_linux/test_gpu.py | 100 ------ tests/_utils/tests/stages/_linux/test_omp.py | 163 ---------- tests/_utils/tests/stages/test_test_stage.py | 87 ------ tests/_utils/tests/stages/test_util.py | 48 --- tests/_utils/tests/test___init__.py | 73 ----- tests/_utils/tests/test_args.py | 132 -------- tests/_utils/tests/test_config.py | 177 ----------- tests/_utils/tests/test_logger.py | 74 ----- tests/_utils/tests/test_system.py | 78 ----- tests/_utils/tests/test_types.py | 30 -- tests/_utils/tests/test_ui.py | 103 ------- tests/_utils/types.py | 50 --- tests/_utils/ui.py | 229 -------------- 38 files changed, 4 insertions(+), 3559 deletions(-) delete mode 100644 tests/_utils/__init__.py delete mode 100644 tests/_utils/args.py delete mode 100644 tests/_utils/config.py delete mode 100644 tests/_utils/logger.py delete mode 100644 tests/_utils/stages/__init__.py delete mode 100644 tests/_utils/stages/_linux/__init__.py delete mode 100644 tests/_utils/stages/_linux/cpu.py delete mode 100644 tests/_utils/stages/_linux/eager.py delete mode 100644 tests/_utils/stages/_linux/gpu.py delete mode 100644 tests/_utils/stages/_linux/omp.py delete mode 100644 tests/_utils/stages/_osx/__init__.py delete mode 100644 tests/_utils/stages/_osx/cpu.py delete mode 100644 tests/_utils/stages/_osx/eager.py delete mode 100644 tests/_utils/stages/_osx/gpu.py delete mode 100644 tests/_utils/stages/_osx/omp.py delete mode 100644 tests/_utils/stages/test_stage.py delete mode 100644 tests/_utils/stages/util.py delete mode 100644 tests/_utils/system.py delete mode 100644 tests/_utils/test_plan.py delete mode 100644 tests/_utils/tests/__init__.py delete mode 100644 tests/_utils/tests/stages/__init__.py delete mode 100644 tests/_utils/tests/stages/_linux/__init__.py delete mode 100644 tests/_utils/tests/stages/_linux/test_cpu.py delete mode 100644 tests/_utils/tests/stages/_linux/test_eager.py delete mode 100644 tests/_utils/tests/stages/_linux/test_gpu.py delete mode 100644 tests/_utils/tests/stages/_linux/test_omp.py delete mode 100644 tests/_utils/tests/stages/test_test_stage.py delete mode 100644 tests/_utils/tests/stages/test_util.py delete mode 100644 tests/_utils/tests/test___init__.py delete mode 100644 tests/_utils/tests/test_args.py delete mode 100644 tests/_utils/tests/test_config.py delete mode 100644 tests/_utils/tests/test_logger.py delete mode 100644 tests/_utils/tests/test_system.py delete mode 100644 tests/_utils/tests/test_types.py delete mode 100644 tests/_utils/tests/test_ui.py delete mode 100644 tests/_utils/types.py delete mode 100644 tests/_utils/ui.py diff --git a/test.py b/test.py index edf9d772b..8dcda54be 100755 --- a/test.py +++ b/test.py @@ -18,14 +18,14 @@ import sys -from tests._utils.config import Config -from tests._utils.system import System -from tests._utils.test_plan import TestPlan +from legate.tester.config import Config +from legate.tester.test_plan import TestPlan +from legate.tester.test_system import TestSystem if __name__ == "__main__": config = Config(sys.argv) - system = System(dry_run=config.dry_run) + system = TestSystem(dry_run=config.dry_run) plan = TestPlan(config, system) diff --git a/tests/_utils/__init__.py b/tests/_utils/__init__.py deleted file mode 100644 index 11b8f1d70..000000000 --- a/tests/_utils/__init__.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Utilities and helpers for implementing the Cunumeric custom test runner. - -""" -from __future__ import annotations - -from typing import Union -from typing_extensions import Literal, TypeAlias - -#: Define the available feature types for tests -FeatureType: TypeAlias = Union[ - Literal["cpus"], Literal["cuda"], Literal["eager"], Literal["openmp"] -] - -#: Value to use if --cpus is not specified. -DEFAULT_CPUS_PER_NODE = 4 - -#: Value to use if --gpus is not specified. -DEFAULT_GPUS_PER_NODE = 1 - -# Delay to introduce between GPU test invocations (ms) -DEFAULT_GPU_DELAY = 2000 - -# Value to use if --fbmem is not specified (MB) -DEFAULT_GPU_MEMORY_BUDGET = 4096 - -#: Value to use if --omps is not specified. -DEFAULT_OMPS_PER_NODE = 1 - -#: Value to use if --ompthreads is not specified. -DEFAULT_OMPTHREADS = 4 - -#: Default values to apply to normalize the testing environment. -DEFAULT_PROCESS_ENV = { - "LEGATE_TEST": "1", -} - -#: Width for terminal ouput headers and footers. -UI_WIDTH = 65 - -#: Feature values that are accepted for --use, in the relative order -#: that the corresponding test stages should always execute in -FEATURES: tuple[FeatureType, ...] = ( - "cpus", - "cuda", - "eager", - "openmp", -) - -#: Paths to example files that should be skipped. -SKIPPED_EXAMPLES = { - "examples/ingest.py", - "examples/kmeans_sort.py", - "examples/lstm_full.py", - "examples/wgrad.py", -} - -#: Extra arguments to supply when specific examples are executed. -PER_FILE_ARGS = { - "examples/lstm_full.py": ["--file", "resources/lstm_input.txt"], -} diff --git a/tests/_utils/args.py b/tests/_utils/args.py deleted file mode 100644 index d97ebf603..000000000 --- a/tests/_utils/args.py +++ /dev/null @@ -1,286 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Provide an argparse ArgumentParser for the test runner. - -""" -from __future__ import annotations - -from argparse import Action, ArgumentParser, Namespace -from typing import ( - Any, - Generic, - Iterable, - Iterator, - Literal, - Sequence, - TypeVar, - Union, -) - -from typing_extensions import TypeAlias - -from . import ( - DEFAULT_CPUS_PER_NODE, - DEFAULT_GPU_DELAY, - DEFAULT_GPU_MEMORY_BUDGET, - DEFAULT_GPUS_PER_NODE, - DEFAULT_OMPS_PER_NODE, - DEFAULT_OMPTHREADS, - FEATURES, -) - -T = TypeVar("T") - -PinOptionsType: TypeAlias = Union[ - Literal["partial"], - Literal["none"], - Literal["strict"], -] - -PIN_OPTIONS: tuple[PinOptionsType, ...] = ( - "partial", - "none", - "strict", -) - - -class MultipleChoices(Generic[T]): - """A container that reports True for any item or subset inclusion. - - Parameters - ---------- - choices: Iterable[T] - The values to populate the containter. - - Examples - -------- - - >>> choices = MultipleChoices(["a", "b", "c"]) - - >>> "a" in choices - True - - >>> ("b", "c") in choices - True - - """ - - def __init__(self, choices: Iterable[T]) -> None: - self.choices = set(choices) - - def __contains__(self, x: Union[T, Iterable[T]]) -> bool: - if isinstance(x, (list, tuple)): - return set(x).issubset(self.choices) - return x in self.choices - - def __iter__(self) -> Iterator[T]: - return self.choices.__iter__() - - -class ExtendAction(Action): - """A custom argparse action to collect multiple values into a list.""" - - def __call__( - self, - parser: ArgumentParser, - namespace: Namespace, - values: Union[str, Sequence[Any], None], - option_string: Union[str, None] = None, - ) -> None: - items = getattr(namespace, self.dest, None) or [] - if isinstance(values, list): - items.extend(values) - else: - items.append(values) - setattr(namespace, self.dest, items) - - -#: The argument parser for test.py -parser = ArgumentParser( - description="Run the Cunumeric test suite", - epilog="Any extra arguments will be forwarded to the Legate script", -) - - -stages = parser.add_argument_group("Feature stage selection") - - -stages.add_argument( - "--use", - dest="features", - action=ExtendAction, - choices=MultipleChoices(sorted(FEATURES)), - # argpase evidently only expects string returns from the type converter - # here, but returning a list of strings seems to work in practice - type=lambda s: s.split(","), # type: ignore[return-value, arg-type] - help="Test Legate with features (also via USE_*)", -) - - -selection = parser.add_argument_group("Test file selection") - - -selection.add_argument( - "--files", - nargs="+", - default=None, - help="Explicit list of test files to run", -) - - -selection.add_argument( - "--unit", - dest="unit", - action="store_true", - default=False, - help="Include unit tests", -) - - -feature_opts = parser.add_argument_group("Feature stage configuration options") - - -feature_opts.add_argument( - "--cpus", - dest="cpus", - type=int, - default=DEFAULT_CPUS_PER_NODE, - help="Number of CPUs per node to use", -) - - -feature_opts.add_argument( - "--gpus", - dest="gpus", - type=int, - default=DEFAULT_GPUS_PER_NODE, - help="Number of GPUs per node to use", -) - - -feature_opts.add_argument( - "--omps", - dest="omps", - type=int, - default=DEFAULT_OMPS_PER_NODE, - help="Number OpenMP processors per node to use", -) - - -feature_opts.add_argument( - "--utility", - dest="utility", - type=int, - default=1, - help="Number of of utility CPUs to reserve for runtime services", -) - - -feature_opts.add_argument( - "--cpu-pin", - dest="cpu_pin", - choices=PIN_OPTIONS, - default="partial", - help="CPU pinning behavior on platforms that support CPU pinning", -) - -feature_opts.add_argument( - "--gpu-delay", - dest="gpu_delay", - type=int, - default=DEFAULT_GPU_DELAY, - help="Delay to introduce between GPU tests (ms)", -) - - -feature_opts.add_argument( - "--fbmem", - dest="fbmem", - type=int, - default=DEFAULT_GPU_MEMORY_BUDGET, - help="GPU framebuffer memory (MB)", -) - - -feature_opts.add_argument( - "--ompthreads", - dest="ompthreads", - metavar="THREADS", - type=int, - default=DEFAULT_OMPTHREADS, - help="Number of threads per OpenMP processor", -) - - -test_opts = parser.add_argument_group("Test run configuration options") - - -test_opts.add_argument( - "--legate", - dest="legate_dir", - metavar="LEGATE_DIR", - action="store", - default=None, - required=False, - help="Path to Legate installation directory", -) - - -test_opts.add_argument( - "-C", - "--directory", - dest="test_root", - metavar="DIR", - action="store", - default=None, - required=False, - help="Root directory containing the tests subdirectory", -) - - -test_opts.add_argument( - "-j", - "--workers", - dest="workers", - type=int, - default=None, - help="Number of parallel workers for testing", -) - - -test_opts.add_argument( - "-v", - "--verbose", - dest="verbose", - action="count", - default=0, - help="Display verbose output. Use -vv for even more output (test stdout)", -) - - -test_opts.add_argument( - "--dry-run", - dest="dry_run", - action="store_true", - help="Print the test plan but don't run anything", -) - - -test_opts.add_argument( - "--debug", - dest="debug", - action="store_true", - help="Print out the commands that are to be executed", -) diff --git a/tests/_utils/config.py b/tests/_utils/config.py deleted file mode 100644 index 06b61e9de..000000000 --- a/tests/_utils/config.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -import os -from argparse import Namespace -from pathlib import Path - -from . import DEFAULT_PROCESS_ENV, FEATURES, SKIPPED_EXAMPLES, FeatureType -from .args import parser -from .types import ArgList, EnvDict - - -class Config: - """A centralized configuration object that provides the information - needed by test stages in order to run. - - Parameters - ---------- - argv : ArgList - command-line arguments to use when building the configuration - - """ - - def __init__(self, argv: ArgList) -> None: - args, self._extra_args = parser.parse_known_args(argv[1:]) - - # which tests to run - self.examples = True - self.integration = True - self.unit = args.unit - self.files = args.files - - # feature configuration - self.features = self._compute_features(args) - - # feature options for integration tests - self.cpus = args.cpus - self.gpus = args.gpus - self.omps = args.omps - self.utility = args.utility - self.cpu_pin = args.cpu_pin - self.fbmem = args.fbmem - self.gpu_delay = args.gpu_delay - self.ompthreads = args.ompthreads - - # test run configuration - self.debug = args.debug - self.dry_run = args.dry_run - self.verbose = args.verbose - self.test_root = args.test_root - self.requested_workers = args.workers - self.legate_dir = self._compute_legate_dir(args) - - @property - def env(self) -> EnvDict: - """Custom environment settings used for process exectution.""" - return dict(DEFAULT_PROCESS_ENV) - - @property - def extra_args(self) -> ArgList: - """Extra command-line arguments to pass on to individual test files.""" - return self._extra_args - - @property - def root_dir(self) -> Path: - """Path to the directory containing the tests.""" - if self.test_root: - return Path(self.test_root) - return Path(__file__).parents[2] - - @property - def test_files(self) -> tuple[Path, ...]: - """List of all test files to use for each stage. - - An explicit list of files from the command line will take precedence. - - Otherwise, the files are computed based on command-line options, etc. - - """ - if self.files: - return self.files - - files = [] - - if self.examples: - examples = ( - path.relative_to(self.root_dir) - for path in self.root_dir.joinpath("examples").glob("*.py") - if str(path.relative_to(self.root_dir)) not in SKIPPED_EXAMPLES - ) - files.extend(sorted(examples)) - - if self.integration: - integration_tests = ( - path.relative_to(self.root_dir) - for path in self.root_dir.joinpath("tests/integration").glob( - "*.py" - ) - ) - files.extend(sorted(integration_tests)) - - if self.unit: - unit_tests = ( - path.relative_to(self.root_dir) - for path in self.root_dir.joinpath("tests/unit").glob( - "**/*.py" - ) - ) - files.extend(sorted(unit_tests)) - - return tuple(files) - - @property - def legate_path(self) -> str: - """Computed path to the legate driver script""" - if self.legate_dir is None: - return "legate" - return str(self.legate_dir / "bin" / "legate") - - def _compute_features(self, args: Namespace) -> tuple[FeatureType, ...]: - if args.features is not None: - computed = args.features - else: - computed = [ - feature - for feature in FEATURES - if os.environ.get(f"USE_{feature.upper()}", None) == "1" - ] - - # if nothing is specified any other way, at least run CPU stage - if len(computed) == 0: - computed.append("cpus") - - return tuple(computed) - - def _compute_legate_dir(self, args: Namespace) -> Path: - # self._legate_source below is purely for testing - if args.legate_dir: - self._legate_source = "cmd" - return Path(args.legate_dir) - elif "LEGATE_DIR" in os.environ: - self._legate_source = "env" - return Path(os.environ["LEGATE_DIR"]) - self._legate_source = "install" - return None diff --git a/tests/_utils/logger.py b/tests/_utils/logger.py deleted file mode 100644 index f40904219..000000000 --- a/tests/_utils/logger.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Provide a basic logger that can scrub ANSI color codes. - -""" -from __future__ import annotations - -import re - -# ref: https://stackoverflow.com/a/14693789 -_ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") - - -class Log: - def __init__(self) -> None: - self._record: list[str] = [] - - def __call__(self, *lines: str) -> tuple[int, int]: - return self.record(*lines) - - def record(self, *lines: str) -> tuple[int, int]: - if len(lines) == 1 and "\n" in lines[0]: - lines = tuple(lines[0].split("\n")) - - start = len(self._record) - for line in lines: - self._record.append(line) - print(line, flush=True) - return (start, len(self._record)) - - def clear(self) -> None: - self._record = [] - - def dump( - self, - *, - start: int = 0, - end: int | None = None, - filter_ansi: bool = True, - ) -> str: - lines = self._record[start:end] - - if filter_ansi: - full_text = _ANSI_ESCAPE.sub("", "\n".join(lines)) - else: - full_text = "\n".join(lines) - - return full_text - - @property - def lines(self) -> tuple[str, ...]: - return tuple(self._record) - - -LOG = Log() diff --git a/tests/_utils/stages/__init__.py b/tests/_utils/stages/__init__.py deleted file mode 100644 index fa8f916d5..000000000 --- a/tests/_utils/stages/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Provide TestStage subclasses for running configured test files using -specific features. - -""" -from __future__ import annotations - -import sys -from typing import Dict, Type - -from .. import FeatureType -from .test_stage import TestStage -from .util import log_proc - -if sys.platform == "darwin": - from ._osx import CPU, Eager, GPU, OMP -elif sys.platform.startswith("linux"): - from ._linux import CPU, Eager, GPU, OMP -else: - raise RuntimeError(f"unsupported platform: {sys.platform}") - -#: All the available test stages that can be selected -STAGES: Dict[FeatureType, Type[TestStage]] = { - "cpus": CPU, - "cuda": GPU, - "openmp": OMP, - "eager": Eager, -} diff --git a/tests/_utils/stages/_linux/__init__.py b/tests/_utils/stages/_linux/__init__.py deleted file mode 100644 index 032305f9c..000000000 --- a/tests/_utils/stages/_linux/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Provide TestStage subclasses for running configured test files using -specific features on linux platforms. - -""" -from __future__ import annotations - -from .cpu import CPU -from .gpu import GPU -from .eager import Eager -from .omp import OMP diff --git a/tests/_utils/stages/_linux/cpu.py b/tests/_utils/stages/_linux/cpu.py deleted file mode 100644 index 665793081..000000000 --- a/tests/_utils/stages/_linux/cpu.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -from itertools import chain - -from ... import FeatureType -from ...config import Config -from ...system import System -from ...types import ArgList, EnvDict -from ..test_stage import TestStage -from ..util import ( - CUNUMERIC_TEST_ARG, - UNPIN_ENV, - Shard, - StageSpec, - adjust_workers, -) - - -class CPU(TestStage): - """A test stage for exercising CPU features. - - Parameters - ---------- - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - - kind: FeatureType = "cpus" - - args = [CUNUMERIC_TEST_ARG] - - def __init__(self, config: Config, system: System) -> None: - self._init(config, system) - - def env(self, config: Config, system: System) -> EnvDict: - return {} if config.cpu_pin == "strict" else dict(UNPIN_ENV) - - def shard_args(self, shard: Shard, config: Config) -> ArgList: - args = [ - "--cpus", - str(config.cpus), - ] - if config.cpu_pin != "none": - args += [ - "--cpu-bind", - ",".join(str(x) for x in shard), - ] - return args - - def compute_spec(self, config: Config, system: System) -> StageSpec: - cpus = system.cpus - - procs = config.cpus + config.utility + int(config.cpu_pin == "strict") - workers = adjust_workers(len(cpus) // procs, config.requested_workers) - - shards: list[tuple[int, ...]] = [] - for i in range(workers): - shard_cpus = range(i * procs, (i + 1) * procs) - shard = chain.from_iterable(cpus[j].ids for j in shard_cpus) - shards.append(tuple(sorted(shard))) - - return StageSpec(workers, shards) diff --git a/tests/_utils/stages/_linux/eager.py b/tests/_utils/stages/_linux/eager.py deleted file mode 100644 index 8e63fc49b..000000000 --- a/tests/_utils/stages/_linux/eager.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -from ... import FeatureType -from ...config import Config -from ...system import System -from ...types import ArgList, EnvDict -from ..test_stage import TestStage -from ..util import Shard, StageSpec, adjust_workers - - -class Eager(TestStage): - """A test stage for exercising Eager Numpy execution features. - - Parameters - ---------- - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - - kind: FeatureType = "eager" - - args: ArgList = [] - - def __init__(self, config: Config, system: System) -> None: - self._init(config, system) - - def env(self, config: Config, system: System) -> EnvDict: - # Raise min chunk sizes for deferred codepaths to force eager execution - env = { - "CUNUMERIC_MIN_CPU_CHUNK": "2000000000", - "CUNUMERIC_MIN_OMP_CHUNK": "2000000000", - "CUNUMERIC_MIN_GPU_CHUNK": "2000000000", - } - return env - - def shard_args(self, shard: Shard, config: Config) -> ArgList: - return [ - "--cpus", - "1", - "--cpu-bind", - ",".join(str(x) for x in shard), - ] - - def compute_spec(self, config: Config, system: System) -> StageSpec: - N = len(system.cpus) - - degree = min(N, 60) # ~LEGION_MAX_NUM_PROCS just in case - workers = adjust_workers(degree, config.requested_workers) - - # Just put each worker on its own full CPU for eager tests - shards = [cpu.ids for cpu in system.cpus] - - return StageSpec(workers, shards) diff --git a/tests/_utils/stages/_linux/gpu.py b/tests/_utils/stages/_linux/gpu.py deleted file mode 100644 index 12012a481..000000000 --- a/tests/_utils/stages/_linux/gpu.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -import time - -from ... import FeatureType -from ...config import Config -from ...system import System -from ...types import ArgList, EnvDict -from ..test_stage import TestStage -from ..util import CUNUMERIC_TEST_ARG, Shard, StageSpec, adjust_workers - -BLOAT_FACTOR = 1.5 # hard coded for now - - -class GPU(TestStage): - """A test stage for exercising GPU features. - - Parameters - ---------- - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - - kind: FeatureType = "cuda" - - args = [CUNUMERIC_TEST_ARG] - - def __init__(self, config: Config, system: System) -> None: - self._init(config, system) - - def env(self, config: Config, system: System) -> EnvDict: - return {} - - def delay(self, shard: Shard, config: Config, system: System) -> None: - time.sleep(config.gpu_delay / 1000) - - def shard_args(self, shard: Shard, config: Config) -> ArgList: - return [ - "--fbmem", - str(config.fbmem), - "--gpus", - str(len(shard)), - "--gpu-bind", - ",".join(str(x) for x in shard), - ] - - def compute_spec(self, config: Config, system: System) -> StageSpec: - N = len(system.gpus) - degree = N // config.gpus - - fbsize = min(gpu.total for gpu in system.gpus) / (2 << 20) # MB - oversub_factor = int(fbsize // (config.fbmem * BLOAT_FACTOR)) - workers = adjust_workers( - degree * oversub_factor, config.requested_workers - ) - - # https://docs.python.org/3/library/itertools.html#itertools-recipes - # grouper('ABCDEF', 3) --> ABC DEF - args = [iter(range(degree * config.gpus))] * config.gpus - per_worker_shards = list(zip(*args)) - - shards = per_worker_shards * workers - - return StageSpec(workers, shards) diff --git a/tests/_utils/stages/_linux/omp.py b/tests/_utils/stages/_linux/omp.py deleted file mode 100644 index 84a954412..000000000 --- a/tests/_utils/stages/_linux/omp.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -from itertools import chain - -from ... import FeatureType -from ...config import Config -from ...system import System -from ...types import ArgList, EnvDict -from ..test_stage import TestStage -from ..util import ( - CUNUMERIC_TEST_ARG, - UNPIN_ENV, - Shard, - StageSpec, - adjust_workers, -) - - -class OMP(TestStage): - """A test stage for exercising OpenMP features. - - Parameters - ---------- - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - - kind: FeatureType = "openmp" - - args = [CUNUMERIC_TEST_ARG] - - def __init__(self, config: Config, system: System) -> None: - self._init(config, system) - - def env(self, config: Config, system: System) -> EnvDict: - return {} if config.cpu_pin == "strict" else dict(UNPIN_ENV) - - def shard_args(self, shard: Shard, config: Config) -> ArgList: - args = [ - "--omps", - str(config.omps), - "--ompthreads", - str(config.ompthreads), - ] - if config.cpu_pin != "none": - args += [ - "--cpu-bind", - ",".join(str(x) for x in shard), - ] - return args - - def compute_spec(self, config: Config, system: System) -> StageSpec: - cpus = system.cpus - omps, threads = config.omps, config.ompthreads - procs = ( - omps * threads + config.utility + int(config.cpu_pin == "strict") - ) - workers = adjust_workers(len(cpus) // procs, config.requested_workers) - - shards: list[tuple[int, ...]] = [] - for i in range(workers): - shard_cpus = range(i * procs, (i + 1) * procs) - shard = chain.from_iterable(cpus[j].ids for j in shard_cpus) - shards.append(tuple(sorted(shard))) - - return StageSpec(workers, shards) diff --git a/tests/_utils/stages/_osx/__init__.py b/tests/_utils/stages/_osx/__init__.py deleted file mode 100644 index 80a7c368d..000000000 --- a/tests/_utils/stages/_osx/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Provide TestStage subclasses for running configured test files using -specific features on OSX. - -""" -from __future__ import annotations - -from .cpu import CPU -from .gpu import GPU -from .eager import Eager -from .omp import OMP diff --git a/tests/_utils/stages/_osx/cpu.py b/tests/_utils/stages/_osx/cpu.py deleted file mode 100644 index ec6d23f20..000000000 --- a/tests/_utils/stages/_osx/cpu.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -from ... import FeatureType -from ...config import Config -from ...system import System -from ...types import ArgList, EnvDict -from ..test_stage import TestStage -from ..util import ( - CUNUMERIC_TEST_ARG, - UNPIN_ENV, - Shard, - StageSpec, - adjust_workers, -) - - -class CPU(TestStage): - """A test stage for exercising CPU features. - - Parameters - ---------- - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - - kind: FeatureType = "cpus" - - args = [CUNUMERIC_TEST_ARG] - - def __init__(self, config: Config, system: System) -> None: - self._init(config, system) - - def env(self, config: Config, system: System) -> EnvDict: - return UNPIN_ENV - - def shard_args(self, shard: Shard, config: Config) -> ArgList: - return ["--cpus", str(config.cpus)] - - def compute_spec(self, config: Config, system: System) -> StageSpec: - procs = config.cpus + config.utility - workers = adjust_workers( - len(system.cpus) // procs, config.requested_workers - ) - - # return a dummy set of shards just for the runner to iterate over - return StageSpec(workers, [(i,) for i in range(workers)]) diff --git a/tests/_utils/stages/_osx/eager.py b/tests/_utils/stages/_osx/eager.py deleted file mode 100644 index 5cc5d557d..000000000 --- a/tests/_utils/stages/_osx/eager.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -from ... import FeatureType -from ...config import Config -from ...system import System -from ...types import ArgList, EnvDict -from ..test_stage import TestStage -from ..util import UNPIN_ENV, Shard, StageSpec, adjust_workers - - -class Eager(TestStage): - """A test stage for exercising Eager Numpy execution features. - - Parameters - ---------- - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - - kind: FeatureType = "eager" - - args: ArgList = [] - - def __init__(self, config: Config, system: System) -> None: - self._init(config, system) - - def env(self, config: Config, system: System) -> EnvDict: - # Raise min chunk sizes for deferred codepaths to force eager execution - env = { - "CUNUMERIC_MIN_CPU_CHUNK": "2000000000", - "CUNUMERIC_MIN_OMP_CHUNK": "2000000000", - "CUNUMERIC_MIN_GPU_CHUNK": "2000000000", - } - env.update(UNPIN_ENV) - return env - - def shard_args(self, shard: Shard, config: Config) -> ArgList: - return ["--cpus", "1"] - - def compute_spec(self, config: Config, system: System) -> StageSpec: - N = len(system.cpus) - degree = min(N, 60) # ~LEGION_MAX_NUM_PROCS just in case - workers = adjust_workers(degree, config.requested_workers) - - # return a dummy set of shards just for the runner to iterate over - return StageSpec(workers, [(i,) for i in range(workers)]) diff --git a/tests/_utils/stages/_osx/gpu.py b/tests/_utils/stages/_osx/gpu.py deleted file mode 100644 index f89fe7377..000000000 --- a/tests/_utils/stages/_osx/gpu.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -import time - -from ... import FeatureType -from ...config import Config -from ...system import System -from ...types import ArgList, EnvDict -from ..test_stage import TestStage -from ..util import CUNUMERIC_TEST_ARG, UNPIN_ENV, Shard - - -class GPU(TestStage): - """A test stage for exercising GPU features. - - Parameters - ---------- - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - - kind: FeatureType = "cuda" - - args: ArgList = [CUNUMERIC_TEST_ARG] - - def __init__(self, config: Config, system: System) -> None: - raise RuntimeError("GPU test are not supported on OSX") - - def env(self, config: Config, system: System) -> EnvDict: - return UNPIN_ENV - - def delay(self, shard: Shard, config: Config, system: System) -> None: - time.sleep(config.gpu_delay / 1000) diff --git a/tests/_utils/stages/_osx/omp.py b/tests/_utils/stages/_osx/omp.py deleted file mode 100644 index f5f19194d..000000000 --- a/tests/_utils/stages/_osx/omp.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -from ... import FeatureType -from ...config import Config -from ...system import System -from ...types import ArgList, EnvDict -from ..test_stage import TestStage -from ..util import ( - CUNUMERIC_TEST_ARG, - UNPIN_ENV, - Shard, - StageSpec, - adjust_workers, -) - - -class OMP(TestStage): - """A test stage for exercising OpenMP features. - - Parameters - ---------- - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - - kind: FeatureType = "openmp" - - args = [CUNUMERIC_TEST_ARG] - - def __init__(self, config: Config, system: System) -> None: - self._init(config, system) - - def env(self, config: Config, system: System) -> EnvDict: - return UNPIN_ENV - - def shard_args(self, shard: Shard, config: Config) -> ArgList: - return [ - "--omps", - str(config.omps), - "--ompthreads", - str(config.ompthreads), - ] - - def compute_spec(self, config: Config, system: System) -> StageSpec: - omps, threads = config.omps, config.ompthreads - procs = omps * threads + config.utility - workers = adjust_workers( - len(system.cpus) // procs, config.requested_workers - ) - - # return a dummy set of shards just for the runner to iterate over - return StageSpec(workers, [(i,) for i in range(workers)]) diff --git a/tests/_utils/stages/test_stage.py b/tests/_utils/stages/test_stage.py deleted file mode 100644 index 0bfbe4f06..000000000 --- a/tests/_utils/stages/test_stage.py +++ /dev/null @@ -1,265 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -import multiprocessing -from datetime import datetime -from pathlib import Path - -from typing_extensions import Protocol - -from .. import PER_FILE_ARGS, FeatureType -from ..config import Config -from ..system import ProcessResult, System -from ..types import ArgList, EnvDict -from ..ui import banner, summary, yellow -from .util import Shard, StageResult, StageSpec, log_proc - - -class TestStage(Protocol): - """Encapsulate running configured test files using specific features. - - Parameters - ---------- - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - - kind: FeatureType - - #: The computed specification for processes to launch to run the - #: configured test files. - spec: StageSpec - - #: The computed sharding id sets to use for job runs - shards: multiprocessing.Queue[Shard] - - #: After the stage completes, results will be stored here - result: StageResult - - #: Any fixed stage-specific command-line args to pass - args: ArgList - - # --- Protocol methods - - def __init__(self, config: Config, system: System) -> None: - ... - - def env(self, config: Config, system: System) -> EnvDict: - """Generate stage-specific customizations to the process env - - Parameters - ---------- - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - ... - - def delay(self, shard: Shard, config: Config, system: System) -> None: - """Wait any delay that should be applied before running the next - test. - - Parameters - ---------- - shard: Shard - The shard to be used for the next test that is run - - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - ... - - def shard_args(self, shard: Shard, config: Config) -> ArgList: - """Generate the command line arguments necessary to launch - the next test process on the given shard. - - Parameters - ---------- - shard: Shard - The shard to be used for the next test that is run - - config: Config - Test runner configuration - - """ - ... - - def compute_spec(self, config: Config, system: System) -> StageSpec: - """Compute the number of worker processes to launch and stage shards - to use for running the configured test files. - - Parameters - ---------- - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - ... - - # --- Shared implementation methods - - def __call__(self, config: Config, system: System) -> None: - """Execute this test stage. - - Parameters - ---------- - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - t0 = datetime.now() - procs = self._launch(config, system) - t1 = datetime.now() - - self.result = StageResult(procs, t1 - t0) - - @property - def name(self) -> str: - """A stage name to display for tests in this stage.""" - return self.__class__.__name__ - - @property - def intro(self) -> str: - """An informative banner to display at stage end.""" - workers = self.spec.workers - workers_text = f"{workers} worker{'s' if workers > 1 else ''}" - return ( - banner(f"Entering stage: {self.name} (with {workers_text})") + "\n" - ) - - @property - def outro(self) -> str: - """An informative banner to display at stage end.""" - total, passed = self.result.total, self.result.passed - - result = summary(self.name, total, passed, self.result.time) - - footer = banner( - f"Exiting stage: {self.name}", - details=( - "* Results : " - + yellow( - f"{passed} / {total} files passed " # noqa E500 - f"({passed/total*100:0.1f}%)" - if total > 0 - else "0 tests are running, Please check " - ), - "* Elapsed time : " + yellow(f"{self.result.time}"), - ), - ) - - return f"{result}\n{footer}" - - def file_args(self, test_file: Path, config: Config) -> ArgList: - """Extra command line arguments based on the test file. - - Parameters - ---------- - test_file : Path - Path to a test file - - config: Config - Test runner configuration - - """ - test_file_string = str(test_file) - args = PER_FILE_ARGS.get(test_file_string, []) - - # These are a bit ugly but necessary in order to make pytest generate - # more verbose output for integration tests when -v, -vv is specified - if "integration" in test_file_string and config.verbose > 0: - args += ["-v"] - if "integration" in test_file_string and config.verbose > 1: - args += ["-s"] - - return args - - def run( - self, test_file: Path, config: Config, system: System - ) -> ProcessResult: - """Execute a single test files with appropriate environment and - command-line options for a feature test stage. - - Parameters - ---------- - test_file : Path - Test file to execute - - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - test_path = config.root_dir / test_file - - shard = self.shards.get() - - stage_args = self.args + self.shard_args(shard, config) - file_args = self.file_args(test_file, config) - - cmd = [str(config.legate_path), str(test_path)] - cmd += stage_args + file_args + config.extra_args - - self.delay(shard, config, system) - - result = system.run(cmd, test_file, env=self._env(config, system)) - log_proc(self.name, result, config, verbose=config.verbose) - - self.shards.put(shard) - - return result - - def _env(self, config: Config, system: System) -> EnvDict: - env = dict(config.env) - env.update(self.env(config, system)) - return env - - def _init(self, config: Config, system: System) -> None: - self.spec = self.compute_spec(config, system) - self.shards = system.manager.Queue(len(self.spec.shards)) - for shard in self.spec.shards: - self.shards.put(shard) - - def _launch(self, config: Config, system: System) -> list[ProcessResult]: - - pool = multiprocessing.pool.ThreadPool(self.spec.workers) - - jobs = [ - pool.apply_async(self.run, (path, config, system)) - for path in config.test_files - ] - pool.close() - - return [job.get() for job in jobs] diff --git a/tests/_utils/stages/util.py b/tests/_utils/stages/util.py deleted file mode 100644 index 357474c90..000000000 --- a/tests/_utils/stages/util.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -from dataclasses import dataclass -from datetime import timedelta -from typing import Tuple, Union - -from typing_extensions import TypeAlias - -from ..config import Config -from ..logger import LOG -from ..system import ProcessResult -from ..ui import failed, passed, shell, skipped - -CUNUMERIC_TEST_ARG = "-cunumeric:test" - -UNPIN_ENV = {"REALM_SYNTHETIC_CORE_MAP": ""} - -Shard: TypeAlias = Tuple[int, ...] - - -@dataclass(frozen=True) -class StageSpec: - """Specify the operation of a test run""" - - #: The number of worker processes to start for running tests - workers: int - - # A list of (cpu or gpu) shards to draw on for each test - shards: list[Shard] - - -@dataclass(frozen=True) -class StageResult: - """Collect results from all tests in a TestStage.""" - - #: Individual test process results including return code and stdout. - procs: list[ProcessResult] - - #: Cumulative execution time for all tests in a stage. - time: timedelta - - @property - def total(self) -> int: - """The total number of tests run in this stage.""" - return len(self.procs) - - @property - def passed(self) -> int: - """The number of tests in this stage that passed.""" - return sum(p.returncode == 0 for p in self.procs) - - -def adjust_workers(workers: int, requested_workers: Union[int, None]) -> int: - """Adjust computed workers according to command line requested workers. - - The final number of workers will only be adjusted down by this function. - - Parameters - ---------- - workers: int - The computed number of workers to use - - requested_workers: int | None, optional - Requested number of workers from the user, if supplied (default: None) - - Returns - ------- - int - The number of workers to actually use - - """ - if requested_workers is not None and requested_workers < 0: - raise ValueError("requested workers must be non-negative") - - if requested_workers is not None: - if requested_workers > workers: - raise RuntimeError( - "Requested workers greater than assignable workers" - ) - workers = requested_workers - - if workers == 0: - raise RuntimeError("Current configuration results in zero workers") - - return workers - - -def log_proc( - name: str, proc: ProcessResult, config: Config, *, verbose: bool -) -> None: - """Log a process result according to the current configuration""" - if config.debug or config.dry_run: - LOG(shell(proc.invocation)) - msg = f"({name}) {proc.test_file}" - details = proc.output.split("\n") if verbose else None - if proc.skipped: - LOG(skipped(msg)) - elif proc.returncode == 0: - LOG(passed(msg, details=details)) - else: - LOG(failed(msg, details=details)) diff --git a/tests/_utils/system.py b/tests/_utils/system.py deleted file mode 100644 index 71411b45b..000000000 --- a/tests/_utils/system.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Provide a System class to encapsulate process execution and reporting -system information (number of CPUs present, etc). - -""" -from __future__ import annotations - -import multiprocessing -import os -import sys -from dataclasses import dataclass -from functools import cached_property -from pathlib import Path -from subprocess import PIPE, STDOUT, run as stdlib_run -from typing import Sequence - -from .types import CPUInfo, EnvDict, GPUInfo - - -@dataclass -class ProcessResult: - - #: The command invovation, including relevant environment vars - invocation: str - - # User-friendly test file path to use in reported output - test_file: Path - - #: Whether this process was actually invoked - skipped: bool = False - - #: The returncode from the process - returncode: int = 0 - - #: The collected stdout and stderr output from the process - output: str = "" - - -class System: - """A facade class for system-related functions. - - Parameters - ---------- - dry_run : bool, optional - If True, no commands will be executed, but a log of any commands - submitted to ``run`` will be made. (default: False) - - """ - - def __init__( - self, - *, - dry_run: bool = False, - ) -> None: - self.manager = multiprocessing.Manager() - self.dry_run: bool = dry_run - - def run( - self, - cmd: Sequence[str], - test_file: Path, - *, - env: EnvDict | None = None, - cwd: str | None = None, - ) -> ProcessResult: - """Wrapper for subprocess.run that encapsulates logging. - - Parameters - ---------- - cmd : sequence of str - The command to run, split on whitespace into a sequence - of strings - - test_file : Path - User-friendly test file path to use in reported output - - env : dict[str, str] or None, optional, default: None - Environment variables to apply when running the command - - cwd: str or None, optional, default: None - A current working directory to pass to stdlib ``run``. - - """ - - env = env or {} - - envstr = ( - " ".join(f"{k}={v}" for k, v in env.items()) - + min(len(env), 1) * " " - ) - - invocation = envstr + " ".join(cmd) - - if self.dry_run: - return ProcessResult(invocation, test_file, skipped=True) - - full_env = dict(os.environ) - full_env.update(env) - - proc = stdlib_run( - cmd, cwd=cwd, env=full_env, stdout=PIPE, stderr=STDOUT, text=True - ) - - return ProcessResult( - invocation, - test_file, - returncode=proc.returncode, - output=proc.stdout, - ) - - @cached_property - def cpus(self) -> tuple[CPUInfo, ...]: - """A list of CPUs on the system.""" - - N = multiprocessing.cpu_count() - - if sys.platform == "darwin": - return tuple(CPUInfo((i,)) for i in range(N)) - - sibling_sets: set[tuple[int, ...]] = set() - for i in range(N): - line = open( - f"/sys/devices/system/cpu/cpu{i}/topology/thread_siblings_list" - ).read() - sibling_sets.add( - tuple(sorted(int(x) for x in line.strip().split(","))) - ) - return tuple(CPUInfo(siblings) for siblings in sorted(sibling_sets)) - - @cached_property - def gpus(self) -> tuple[GPUInfo, ...]: - """A list of GPUs on the system, including total memory information.""" - - try: - # This pynvml import is protected inside this method so that in - # case pynvml is not installed, tests stages that don't need gpu - # info (e.g. cpus, eager) will proceed unaffected. Test stages - # that do require gpu info will fail here with an ImportError. - import pynvml # type: ignore[import] - - # Also a pynvml package is available on some platforms that won't - # have GPUs for some reason. In which case this init call will - # fail. - pynvml.nvmlInit() - except Exception: - return () - - num_gpus = pynvml.nvmlDeviceGetCount() - - results = [] - for i in range(num_gpus): - info = pynvml.nvmlDeviceGetMemoryInfo( - pynvml.nvmlDeviceGetHandleByIndex(i) - ) - results.append(GPUInfo(i, info.total)) - - return tuple(results) diff --git a/tests/_utils/test_plan.py b/tests/_utils/test_plan.py deleted file mode 100644 index 9e2a92532..000000000 --- a/tests/_utils/test_plan.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Provide a TestPlan class to coordinate multiple feature test stages. - -""" -from __future__ import annotations - -from datetime import timedelta -from itertools import chain - -from .config import Config -from .logger import LOG -from .stages import STAGES, log_proc -from .system import System -from .ui import banner, rule, summary, yellow - - -class TestPlan: - """Encapsulate an entire test run with multiple feature test stages. - - Parameters - ---------- - config: Config - Test runner configuration - - system: System - Process execution wrapper - - """ - - def __init__(self, config: Config, system: System) -> None: - self._config = config - self._system = system - self._stages = [ - STAGES[feature](config, system) for feature in config.features - ] - - def execute(self) -> int: - """Execute the entire test run with all configured feature stages.""" - LOG.clear() - - LOG(self.intro) - - for stage in self._stages: - LOG(stage.intro) - stage(self._config, self._system) - LOG(stage.outro) - - all_procs = tuple( - chain.from_iterable(s.result.procs for s in self._stages) - ) - total = len(all_procs) - passed = sum(proc.returncode == 0 for proc in all_procs) - - LOG(f"\n{rule()}") - - self._log_failures(total, passed) - - LOG(self.outro(total, passed)) - - return int((total - passed) > 0) - - @property - def intro(self) -> str: - """An informative banner to display at test run start.""" - - cpus = len(self._system.cpus) - try: - gpus = len(self._system.gpus) - except ImportError: - gpus = 0 - - details = ( - f"* Feature stages : {', '.join(yellow(x) for x in self._config.features)}", # noqa E501 - f"* Test files per stage : {yellow(str(len(self._config.test_files)))}", # noqa E501 - f"* System description : {yellow(str(cpus) + ' cpus')} / {yellow(str(gpus) + ' gpus')}", # noqa E501 - ) - return banner("Test Suite Configuration", details=details) - - def outro(self, total: int, passed: int) -> str: - """An informative banner to display at test run end. - - Parameters - ---------- - total: int - Number of total tests that ran in all stages - - passed: int - Number of tests that passed in all stages - - """ - details = [ - f"* {s.name: <6}: " - + yellow( - f"{s.result.passed} / {s.result.total} passed in {s.result.time.total_seconds():0.2f}s" # noqa E501 - ) - for s in self._stages - ] - - time = sum((s.result.time for s in self._stages), timedelta(0, 0)) - details.append("") - details.append( - summary("All tests", total, passed, time, justify=False) - ) - - overall = banner("Overall summary", details=details) - - return f"{overall}\n" - - def _log_failures(self, total: int, passed: int) -> None: - if total == passed: - return - - LOG(f"{banner('FAILURES')}\n") - - for stage in self._stages: - procs = (proc for proc in stage.result.procs if proc.returncode) - for proc in procs: - log_proc(stage.name, proc, self._config, verbose=True) diff --git a/tests/_utils/tests/__init__.py b/tests/_utils/tests/__init__.py deleted file mode 100644 index f0b271624..000000000 --- a/tests/_utils/tests/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations diff --git a/tests/_utils/tests/stages/__init__.py b/tests/_utils/tests/stages/__init__.py deleted file mode 100644 index 6e3992dc1..000000000 --- a/tests/_utils/tests/stages/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -from typing import Any - -from ...system import System -from ...types import CPUInfo, GPUInfo - - -class FakeSystem(System): - def __init__( - self, cpus: int = 6, gpus: int = 6, fbmem: int = 6 << 32, **kwargs: Any - ) -> None: - self._cpus = cpus - self._gpus = gpus - self._fbmem = fbmem - super().__init__(**kwargs) - - @property - def cpus(self) -> tuple[CPUInfo, ...]: - return tuple(CPUInfo((i,)) for i in range(self._cpus)) - - @property - def gpus(self) -> tuple[GPUInfo, ...]: - return tuple(GPUInfo(i, self._fbmem) for i in range(self._gpus)) diff --git a/tests/_utils/tests/stages/_linux/__init__.py b/tests/_utils/tests/stages/_linux/__init__.py deleted file mode 100644 index 345983919..000000000 --- a/tests/_utils/tests/stages/_linux/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -import sys - -import pytest - -if sys.platform != "linux": - pytestmark = pytest.mark.skip() diff --git a/tests/_utils/tests/stages/_linux/test_cpu.py b/tests/_utils/tests/stages/_linux/test_cpu.py deleted file mode 100644 index cc2825c31..000000000 --- a/tests/_utils/tests/stages/_linux/test_cpu.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -import pytest - -from ....config import Config -from ....stages._linux import cpu as m -from ....stages.util import UNPIN_ENV -from .. import FakeSystem - - -def test_default() -> None: - c = Config([]) - s = FakeSystem(cpus=12) - stage = m.CPU(c, s) - assert stage.kind == "cpus" - assert stage.args == ["-cunumeric:test"] - assert stage.env(c, s) == UNPIN_ENV - assert stage.spec.workers > 0 - - shard = (1, 2, 3) - assert "--cpu-bind" in stage.shard_args(shard, c) - - -def test_cpu_pin_strict() -> None: - c = Config(["test.py", "--cpu-pin", "strict"]) - s = FakeSystem(cpus=12) - stage = m.CPU(c, s) - assert stage.kind == "cpus" - assert stage.args == ["-cunumeric:test"] - assert stage.env(c, s) == {} - assert stage.spec.workers > 0 - - shard = (1, 2, 3) - assert "--cpu-bind" in stage.shard_args(shard, c) - - -def test_cpu_pin_none() -> None: - c = Config(["test.py", "--cpu-pin", "none"]) - s = FakeSystem(cpus=12) - stage = m.CPU(c, s) - assert stage.kind == "cpus" - assert stage.args == ["-cunumeric:test"] - assert stage.env(c, s) == UNPIN_ENV - assert stage.spec.workers > 0 - - shard = (1, 2, 3) - assert "--cpu-bind" not in stage.shard_args(shard, c) - - -@pytest.mark.parametrize("shard,expected", [[(2,), "2"], [(1, 2, 3), "1,2,3"]]) -def test_shard_args(shard: tuple[int, ...], expected: str) -> None: - c = Config([]) - s = FakeSystem() - stage = m.CPU(c, s) - result = stage.shard_args(shard, c) - assert result == ["--cpus", f"{c.cpus}", "--cpu-bind", expected] - - -def test_spec_with_cpus_1() -> None: - c = Config(["test.py", "--cpus", "1"]) - s = FakeSystem() - stage = m.CPU(c, s) - assert stage.spec.workers == 3 - assert stage.spec.shards == [(0, 1), (2, 3), (4, 5)] - - -def test_spec_with_cpus_2() -> None: - c = Config(["test.py", "--cpus", "2"]) - s = FakeSystem() - stage = m.CPU(c, s) - assert stage.spec.workers == 2 - assert stage.spec.shards == [(0, 1, 2), (3, 4, 5)] - - -def test_spec_with_utility() -> None: - c = Config(["test.py", "--cpus", "1", "--utility", "2"]) - s = FakeSystem() - stage = m.CPU(c, s) - assert stage.spec.workers == 2 - assert stage.spec.shards == [(0, 1, 2), (3, 4, 5)] - - -def test_spec_with_requested_workers() -> None: - c = Config(["test.py", "--cpus", "1", "-j", "2"]) - s = FakeSystem() - stage = m.CPU(c, s) - assert stage.spec.workers == 2 - assert stage.spec.shards == [(0, 1), (2, 3)] - - -def test_spec_with_requested_workers_zero() -> None: - s = FakeSystem() - c = Config(["test.py", "-j", "0"]) - assert c.requested_workers == 0 - with pytest.raises(RuntimeError): - m.CPU(c, s) - - -def test_spec_with_requested_workers_bad() -> None: - s = FakeSystem() - c = Config(["test.py", "-j", f"{len(s.cpus)+1}"]) - assert c.requested_workers > len(s.cpus) - with pytest.raises(RuntimeError): - m.CPU(c, s) - - -def test_spec_with_verbose() -> None: - args = ["test.py", "--cpus", "2"] - c = Config(args) - cv = Config(args + ["--verbose"]) - s = FakeSystem() - - spec, vspec = m.CPU(c, s).spec, m.CPU(cv, s).spec - assert vspec == spec diff --git a/tests/_utils/tests/stages/_linux/test_eager.py b/tests/_utils/tests/stages/_linux/test_eager.py deleted file mode 100644 index 8fc21ecb6..000000000 --- a/tests/_utils/tests/stages/_linux/test_eager.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -import pytest - -from ....config import Config -from ....stages._linux import eager as m -from .. import FakeSystem - - -def test_default() -> None: - c = Config([]) - s = FakeSystem() - stage = m.Eager(c, s) - assert stage.kind == "eager" - assert stage.args == [] - assert stage.env(c, s) == { - "CUNUMERIC_MIN_CPU_CHUNK": "2000000000", - "CUNUMERIC_MIN_OMP_CHUNK": "2000000000", - "CUNUMERIC_MIN_GPU_CHUNK": "2000000000", - } - assert stage.spec.workers > 0 - - -@pytest.mark.parametrize("shard,expected", [[(2,), "2"], [(1, 2, 3), "1,2,3"]]) -def test_shard_args(shard: tuple[int, ...], expected: str) -> None: - c = Config([]) - s = FakeSystem() - stage = m.Eager(c, s) - result = stage.shard_args(shard, c) - assert result == ["--cpus", "1", "--cpu-bind", expected] - - -def test_spec() -> None: - c = Config([]) - s = FakeSystem() - stage = m.Eager(c, s) - assert stage.spec.workers == len(s.cpus) - # [cpu.ids for cpu in system.cpus] - assert stage.spec.shards == [(i,) for i in range(stage.spec.workers)] - - -def test_spec_with_requested_workers_zero() -> None: - s = FakeSystem() - c = Config(["test.py", "-j", "0"]) - assert c.requested_workers == 0 - with pytest.raises(RuntimeError): - m.Eager(c, s) - - -def test_spec_with_requested_workers_bad() -> None: - s = FakeSystem() - c = Config(["test.py", "-j", f"{len(s.cpus)+1}"]) - assert c.requested_workers > len(s.cpus) - with pytest.raises(RuntimeError): - m.Eager(c, s) - - -def test_spec_with_verbose() -> None: - c = Config(["test.py"]) - cv = Config(["test.py", "--verbose"]) - s = FakeSystem() - - spec, vspec = m.Eager(c, s).spec, m.Eager(cv, s).spec - assert vspec == spec diff --git a/tests/_utils/tests/stages/_linux/test_gpu.py b/tests/_utils/tests/stages/_linux/test_gpu.py deleted file mode 100644 index 13c7bb836..000000000 --- a/tests/_utils/tests/stages/_linux/test_gpu.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -import pytest - -from ....config import Config -from ....stages._linux import gpu as m -from .. import FakeSystem - - -def test_default() -> None: - c = Config([]) - s = FakeSystem() - stage = m.GPU(c, s) - assert stage.kind == "cuda" - assert stage.args == ["-cunumeric:test"] - assert stage.env(c, s) == {} - assert stage.spec.workers > 0 - - -@pytest.mark.parametrize("shard,expected", [[(2,), "2"], [(1, 2, 3), "1,2,3"]]) -def test_shard_args(shard: tuple[int, ...], expected: str) -> None: - c = Config([]) - s = FakeSystem() - stage = m.GPU(c, s) - result = stage.shard_args(shard, c) - assert result == [ - "--fbmem", - "4096", - "--gpus", - f"{len(shard)}", - "--gpu-bind", - expected, - ] - - -def test_spec_with_gpus_1() -> None: - c = Config(["test.py", "--gpus", "1"]) - s = FakeSystem() - stage = m.GPU(c, s) - assert stage.spec.workers == 12 - assert stage.spec.shards == [(0,), (1,), (2,), (3,), (4,), (5,)] * 12 - - -def test_spec_with_gpus_2() -> None: - c = Config(["test.py", "--gpus", "2"]) - s = FakeSystem() - stage = m.GPU(c, s) - assert stage.spec.workers == 6 - assert stage.spec.shards == [(0, 1), (2, 3), (4, 5)] * 6 - - -def test_spec_with_requested_workers() -> None: - c = Config(["test.py", "--gpus", "1", "-j", "2"]) - s = FakeSystem() - stage = m.GPU(c, s) - assert stage.spec.workers == 2 - assert stage.spec.shards == [(0,), (1,), (2,), (3,), (4,), (5,)] * 2 - - -def test_spec_with_requested_workers_zero() -> None: - s = FakeSystem() - c = Config(["test.py", "-j", "0"]) - assert c.requested_workers == 0 - with pytest.raises(RuntimeError): - m.GPU(c, s) - - -def test_spec_with_requested_workers_bad() -> None: - s = FakeSystem() - c = Config(["test.py", "-j", f"{len(s.gpus)+100}"]) - assert c.requested_workers > len(s.gpus) - with pytest.raises(RuntimeError): - m.GPU(c, s) - - -def test_spec_with_verbose() -> None: - args = ["test.py", "--gpus", "2"] - c = Config(args) - cv = Config(args + ["--verbose"]) - s = FakeSystem() - - spec, vspec = m.GPU(c, s).spec, m.GPU(cv, s).spec - assert vspec == spec diff --git a/tests/_utils/tests/stages/_linux/test_omp.py b/tests/_utils/tests/stages/_linux/test_omp.py deleted file mode 100644 index fd836759e..000000000 --- a/tests/_utils/tests/stages/_linux/test_omp.py +++ /dev/null @@ -1,163 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -import pytest - -from ....config import Config -from ....stages._linux import omp as m -from ....stages.util import UNPIN_ENV -from .. import FakeSystem - - -def test_default() -> None: - c = Config([]) - s = FakeSystem(cpus=12) - stage = m.OMP(c, s) - assert stage.kind == "openmp" - assert stage.args == ["-cunumeric:test"] - assert stage.env(c, s) == UNPIN_ENV - assert stage.spec.workers > 0 - - shard = (1, 2, 3) - assert "--cpu-bind" in stage.shard_args(shard, c) - - -def test_cpu_pin_strict() -> None: - c = Config(["test.py", "--cpu-pin", "strict"]) - s = FakeSystem(cpus=12) - stage = m.OMP(c, s) - assert stage.kind == "openmp" - assert stage.args == ["-cunumeric:test"] - assert stage.env(c, s) == {} - assert stage.spec.workers > 0 - - shard = (1, 2, 3) - assert "--cpu-bind" in stage.shard_args(shard, c) - - -def test_cpu_pin_none() -> None: - c = Config(["test.py", "--cpu-pin", "none"]) - s = FakeSystem(cpus=12) - stage = m.OMP(c, s) - assert stage.kind == "openmp" - assert stage.args == ["-cunumeric:test"] - assert stage.env(c, s) == UNPIN_ENV - assert stage.spec.workers > 0 - - shard = (1, 2, 3) - assert "--cpu-bind" not in stage.shard_args(shard, c) - - -@pytest.mark.parametrize("shard,expected", [[(2,), "2"], [(1, 2, 3), "1,2,3"]]) -def test_shard_args(shard: tuple[int, ...], expected: str) -> None: - c = Config([]) - s = FakeSystem(cpus=12) - stage = m.OMP(c, s) - result = stage.shard_args(shard, c) - assert result == [ - "--omps", - f"{c.omps}", - "--ompthreads", - f"{c.ompthreads}", - "--cpu-bind", - expected, - ] - - -def test_spec_with_omps_1_threads_1() -> None: - c = Config(["test.py", "--omps", "1", "--ompthreads", "1"]) - s = FakeSystem(cpus=12) - stage = m.OMP(c, s) - assert stage.spec.workers == 6 - assert stage.spec.shards == [ - (0, 1), - (2, 3), - (4, 5), - (6, 7), - (8, 9), - (10, 11), - ] - - -def test_spec_with_omps_1_threads_2() -> None: - c = Config(["test.py", "--omps", "1", "--ompthreads", "2"]) - s = FakeSystem(cpus=12) - stage = m.OMP(c, s) - assert stage.spec.workers == 4 - assert stage.spec.shards == [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11)] - - -def test_spec_with_omps_2_threads_1() -> None: - c = Config(["test.py", "--omps", "2", "--ompthreads", "1"]) - s = FakeSystem(cpus=12) - stage = m.OMP(c, s) - assert stage.spec.workers == 4 - assert stage.spec.shards == [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11)] - - -def test_spec_with_omps_2_threads_2() -> None: - c = Config(["test.py", "--omps", "2", "--ompthreads", "2"]) - s = FakeSystem(cpus=12) - stage = m.OMP(c, s) - assert stage.spec.workers == 2 - assert stage.spec.shards == [(0, 1, 2, 3, 4), (5, 6, 7, 8, 9)] - - -def test_spec_with_utility() -> None: - c = Config( - ["test.py", "--omps", "2", "--ompthreads", "2", "--utility", "3"] - ) - s = FakeSystem(cpus=12) - stage = m.OMP(c, s) - assert stage.spec.workers == 1 - assert stage.spec.shards == [(0, 1, 2, 3, 4, 5, 6)] - - -def test_spec_with_requested_workers() -> None: - c = Config(["test.py", "--omps", "1", "--ompthreads", "1", "-j", "2"]) - s = FakeSystem(cpus=12) - stage = m.OMP(c, s) - assert stage.spec.workers == 2 - assert stage.spec.shards == [(0, 1), (2, 3)] - - -def test_spec_with_requested_workers_zero() -> None: - s = FakeSystem(cpus=12) - c = Config(["test.py", "-j", "0"]) - assert c.requested_workers == 0 - with pytest.raises(RuntimeError): - m.OMP(c, s) - - -def test_spec_with_requested_workers_bad() -> None: - s = FakeSystem(cpus=12) - c = Config(["test.py", "-j", f"{len(s.cpus)+1}"]) - assert c.requested_workers > len(s.cpus) - with pytest.raises(RuntimeError): - m.OMP(c, s) - - -def test_spec_with_verbose() -> None: - args = ["test.py", "--cpus", "2"] - c = Config(args) - cv = Config(args + ["--verbose"]) - s = FakeSystem(cpus=12) - - spec, vspec = m.OMP(c, s).spec, m.OMP(cv, s).spec - assert vspec == spec diff --git a/tests/_utils/tests/stages/test_test_stage.py b/tests/_utils/tests/stages/test_test_stage.py deleted file mode 100644 index 393ac18bc..000000000 --- a/tests/_utils/tests/stages/test_test_stage.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -from datetime import timedelta -from pathlib import Path - -from ... import FeatureType -from ...config import Config -from ...stages import test_stage as m -from ...stages.util import StageResult, StageSpec -from ...system import ProcessResult, System -from . import FakeSystem - -s = FakeSystem() - - -class MockTestStage(m.TestStage): - - kind: FeatureType = "eager" - - name = "mock" - - args = ["-foo", "-bar"] - - def __init__(self, config: Config, system: System) -> None: - self._init(config, system) - - def compute_spec(self, config: Config, system: System) -> StageSpec: - return StageSpec(2, [(0,), (1,), (2,)]) - - -class TestTestStage: - def test_name(self) -> None: - c = Config([]) - stage = MockTestStage(c, s) - assert stage.name == "mock" - - def test_intro(self) -> None: - c = Config([]) - stage = MockTestStage(c, s) - assert "Entering stage: mock" in stage.intro - - def test_outro(self) -> None: - c = Config([]) - stage = MockTestStage(c, s) - stage.result = StageResult( - [ProcessResult("invoke", Path("test/file"))], - timedelta(seconds=2.123), - ) - outro = stage.outro - assert "Exiting stage: mock" in outro - assert "Passed 1 of 1 tests (100.0%)" in outro - assert "2.123" in outro - - def test_file_args_default(self) -> None: - c = Config([]) - stage = MockTestStage(c, s) - assert stage.file_args(Path("integration/foo"), c) == [] - assert stage.file_args(Path("unit/foo"), c) == [] - - def test_file_args_v(self) -> None: - c = Config(["test.py", "-v"]) - stage = MockTestStage(c, s) - assert stage.file_args(Path("integration/foo"), c) == ["-v"] - assert stage.file_args(Path("unit/foo"), c) == [] - - def test_file_args_vv(self) -> None: - c = Config(["test.py", "-vv"]) - stage = MockTestStage(c, s) - assert stage.file_args(Path("integration/foo"), c) == ["-v", "-s"] - assert stage.file_args(Path("unit/foo"), c) == [] diff --git a/tests/_utils/tests/stages/test_util.py b/tests/_utils/tests/stages/test_util.py deleted file mode 100644 index 7d9dfe143..000000000 --- a/tests/_utils/tests/stages/test_util.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -import pytest - -from ...stages import util as m - - -class Test_adjust_workers: - @pytest.mark.parametrize("n", (1, 5, 100)) - def test_None_requested(self, n: int) -> None: - assert m.adjust_workers(n, None) == n - - @pytest.mark.parametrize("n", (1, 2, 9)) - def test_requested(self, n: int) -> None: - assert m.adjust_workers(10, n) == n - - def test_negative_requested(self) -> None: - with pytest.raises(ValueError): - assert m.adjust_workers(10, -1) - - def test_zero_requested(self) -> None: - with pytest.raises(RuntimeError): - assert m.adjust_workers(10, 0) - - def test_zero_computed(self) -> None: - with pytest.raises(RuntimeError): - assert m.adjust_workers(0, None) - - def test_requested_too_large(self) -> None: - with pytest.raises(RuntimeError): - assert m.adjust_workers(10, 11) diff --git a/tests/_utils/tests/test___init__.py b/tests/_utils/tests/test___init__.py deleted file mode 100644 index 393f5d7bc..000000000 --- a/tests/_utils/tests/test___init__.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -from .. import ( - DEFAULT_CPUS_PER_NODE, - DEFAULT_GPU_DELAY, - DEFAULT_GPU_MEMORY_BUDGET, - DEFAULT_GPUS_PER_NODE, - DEFAULT_OMPS_PER_NODE, - DEFAULT_OMPTHREADS, - DEFAULT_PROCESS_ENV, - FEATURES, - PER_FILE_ARGS, - SKIPPED_EXAMPLES, - UI_WIDTH, -) - - -class TestConsts: - def test_DEFAULT_CPUS_PER_NODE(self) -> None: - assert DEFAULT_CPUS_PER_NODE == 4 - - def test_DEFAULT_GPUS_PER_NODE(self) -> None: - assert DEFAULT_GPUS_PER_NODE == 1 - - def test_DEFAULT_GPU_DELAY(self) -> None: - assert DEFAULT_GPU_DELAY == 2000 - - def test_DEFAULT_GPU_MEMORY_BUDGET(self) -> None: - assert DEFAULT_GPU_MEMORY_BUDGET == 4096 - - def test_DEFAULT_OMPS_PER_NODE(self) -> None: - assert DEFAULT_OMPS_PER_NODE == 1 - - def test_DEFAULT_OMPTHREADS(self) -> None: - assert DEFAULT_OMPTHREADS == 4 - - def test_DEFAULT_PROCESS_ENV(self) -> None: - assert DEFAULT_PROCESS_ENV == { - "LEGATE_TEST": "1", - } - - def test_UI_WIDTH(self) -> None: - assert UI_WIDTH == 65 - - def test_FEATURES(self) -> None: - assert FEATURES == ("cpus", "cuda", "eager", "openmp") - - def test_SKIPPED_EXAMPLES(self) -> None: - assert isinstance(SKIPPED_EXAMPLES, set) - assert all(isinstance(x, str) for x in SKIPPED_EXAMPLES) - assert all(x.startswith("examples") for x in SKIPPED_EXAMPLES) - - def test_PER_FILE_ARGS(self) -> None: - assert isinstance(PER_FILE_ARGS, dict) - assert all(isinstance(x, str) for x in PER_FILE_ARGS.keys()) - assert all(isinstance(x, list) for x in PER_FILE_ARGS.values()) diff --git a/tests/_utils/tests/test_args.py b/tests/_utils/tests/test_args.py deleted file mode 100644 index 1f17a9bdb..000000000 --- a/tests/_utils/tests/test_args.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -from itertools import chain, combinations -from typing import Iterable, TypeVar - -import pytest - -from .. import ( - DEFAULT_CPUS_PER_NODE, - DEFAULT_GPU_DELAY, - DEFAULT_GPU_MEMORY_BUDGET, - DEFAULT_GPUS_PER_NODE, - DEFAULT_OMPS_PER_NODE, - DEFAULT_OMPTHREADS, - args as m, -) - -T = TypeVar("T") - - -# https://docs.python.org/3/library/itertools.html#itertools-recipes -def powerset(iterable: Iterable[T]) -> Iterable[Iterable[T]]: - xs = list(iterable) - return chain.from_iterable(combinations(xs, n) for n in range(len(xs) + 1)) - - -class TestParserDefaults: - def test_featurs(self) -> None: - assert m.parser.get_default("features") is None - - def test_files(self) -> None: - assert m.parser.get_default("files") is None - - def test_unit(self) -> None: - assert m.parser.get_default("unit") is False - - def test_cpus(self) -> None: - assert m.parser.get_default("cpus") == DEFAULT_CPUS_PER_NODE - - def test_gpus(self) -> None: - assert m.parser.get_default("gpus") == DEFAULT_GPUS_PER_NODE - - def test_cpu_pin(self) -> None: - assert m.parser.get_default("cpu_pin") == "partial" - - def test_gpu_delay(self) -> None: - assert m.parser.get_default("gpu_delay") == DEFAULT_GPU_DELAY - - def test_fbmem(self) -> None: - assert m.parser.get_default("fbmem") == DEFAULT_GPU_MEMORY_BUDGET - - def test_omps(self) -> None: - assert m.parser.get_default("omps") == DEFAULT_OMPS_PER_NODE - - def test_ompthreads(self) -> None: - assert m.parser.get_default("ompthreads") == DEFAULT_OMPTHREADS - - def test_legate_dir(self) -> None: - assert m.parser.get_default("legate_dir") is None - - def test_test_root(self) -> None: - assert m.parser.get_default("test_root") is None - - def test_workers(self) -> None: - assert m.parser.get_default("workers") is None - - def test_verbose(self) -> None: - assert m.parser.get_default("verbose") == 0 - - def test_dry_run(self) -> None: - assert m.parser.get_default("dry_run") is False - - def test_debug(self) -> None: - assert m.parser.get_default("debug") is False - - -class TestParserConfig: - def test_parser_epilog(self) -> None: - assert ( - m.parser.epilog - == "Any extra arguments will be forwarded to the Legate script" - ) - - def test_parser_description(self) -> None: - assert m.parser.description == "Run the Cunumeric test suite" - - -class TestMultipleChoices: - @pytest.mark.parametrize("choices", ([1, 2, 3], range(4), ("a", "b"))) - def test_init(self, choices: Iterable[T]) -> None: - mc = m.MultipleChoices(choices) - assert mc.choices == set(choices) - - def test_contains_item(self) -> None: - choices = [1, 2, 3] - mc = m.MultipleChoices(choices) - for item in choices: - assert item in mc - - def test_contains_subset(self) -> None: - choices = [1, 2, 3] - mc = m.MultipleChoices(choices) - for subset in powerset(choices): - assert subset in mc - - def test_iter(self) -> None: - choices = [1, 2, 3] - mc = m.MultipleChoices(choices) - assert list(mc) == choices - - -# Testing this directly would require getting into argparse -# internals. See test_config.py for indirect tests with --use -class TestExtendAction: - pass diff --git a/tests/_utils/tests/test_config.py b/tests/_utils/tests/test_config.py deleted file mode 100644 index 76f71d7e7..000000000 --- a/tests/_utils/tests/test_config.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -from pathlib import Path, PurePath - -import pytest - -from .. import ( - DEFAULT_CPUS_PER_NODE, - DEFAULT_GPU_DELAY, - DEFAULT_GPU_MEMORY_BUDGET, - DEFAULT_GPUS_PER_NODE, - DEFAULT_OMPS_PER_NODE, - DEFAULT_OMPTHREADS, - FEATURES, - config as m, -) -from ..args import PIN_OPTIONS, PinOptionsType - - -class TestConfig: - def test_default_init(self) -> None: - c = m.Config([]) - - assert c.examples is True - assert c.integration is True - assert c.unit is False - assert c.files is None - - assert c.features == ("cpus",) - - assert c.cpus == DEFAULT_CPUS_PER_NODE - assert c.gpus == DEFAULT_GPUS_PER_NODE - assert c.cpu_pin == "partial" - assert c.gpu_delay == DEFAULT_GPU_DELAY - assert c.fbmem == DEFAULT_GPU_MEMORY_BUDGET - assert c.omps == DEFAULT_OMPS_PER_NODE - assert c.ompthreads == DEFAULT_OMPTHREADS - - assert c.debug is False - assert c.dry_run is False - assert c.verbose == 0 - assert c.test_root is None - assert c.requested_workers is None - assert c.legate_dir is None - - assert c.extra_args == [] - assert c.root_dir == PurePath(m.__file__).parents[2] - assert len(c.test_files) > 0 - assert any("examples" in str(x) for x in c.test_files) - assert any("integration" in str(x) for x in c.test_files) - assert all("unit" not in str(x) for x in c.test_files) - assert c.legate_path == "legate" - - @pytest.mark.parametrize("feature", FEATURES) - def test_env_features( - self, monkeypatch: pytest.MonkeyPatch, feature: str - ) -> None: - monkeypatch.setenv(f"USE_{feature.upper()}", "1") - - # test default config - c = m.Config([]) - assert set(c.features) == {feature} - - # also test with a --use value provided - c = m.Config(["test.py", "--use", "cuda"]) - assert set(c.features) == {"cuda"} - - @pytest.mark.parametrize("feature", FEATURES) - def test_cmd_features(self, feature: str) -> None: - - # test a single value - c = m.Config(["test.py", "--use", feature]) - assert set(c.features) == {feature} - - # also test with multiple / duplication - c = m.Config(["test.py", "--use", f"cpus,{feature}"]) - assert set(c.features) == {"cpus", feature} - - def test_unit(self) -> None: - c = m.Config(["test.py", "--unit"]) - assert len(c.test_files) > 0 - assert any("examples" in str(x) for x in c.test_files) - assert any("integration" in str(x) for x in c.test_files) - assert any("unit" in str(x) for x in c.test_files) - - def test_files(self) -> None: - c = m.Config(["test.py", "--files", "a", "b", "c"]) - assert c.files == ["a", "b", "c"] - - @pytest.mark.parametrize( - "opt", ("cpus", "gpus", "gpu-delay", "fbmem", "omps", "ompthreads") - ) - def test_feature_options(self, opt: str) -> None: - c = m.Config(["test.py", f"--{opt}", "1234"]) - assert getattr(c, opt.replace("-", "_")) == 1234 - - @pytest.mark.parametrize("value", PIN_OPTIONS) - def test_cpu_pin(self, value: PinOptionsType) -> None: - c = m.Config(["test.py", "--cpu-pin", value]) - assert c.cpu_pin == value - - def test_workers(self) -> None: - c = m.Config(["test.py", "-j", "1234"]) - assert c.requested_workers == 1234 - - def test_debug(self) -> None: - c = m.Config(["test.py", "--debug"]) - assert c.debug is True - - def test_dry_run(self) -> None: - c = m.Config(["test.py", "--dry-run"]) - assert c.dry_run is True - - @pytest.mark.parametrize("arg", ("-v", "--verbose")) - def test_verbose1(self, arg: str) -> None: - c = m.Config(["test.py", arg]) - assert c.verbose == 1 - - def test_verbose2(self) -> None: - c = m.Config(["test.py", "-vv"]) - assert c.verbose == 2 - - @pytest.mark.parametrize("arg", ("-C", "--directory")) - def test_test_root(self, arg: str) -> None: - c = m.Config(["test.py", arg, "some/path"]) - assert c.test_root == "some/path" - - def test_legate_dir(self) -> None: - c = m.Config([]) - assert c.legate_dir is None - assert c.legate_path == "legate" - assert c._legate_source == "install" - - def test_cmd_legate_dir_good(self) -> None: - legate_dir = Path("/usr/local") - c = m.Config(["test.py", "--legate", str(legate_dir)]) - assert c.legate_dir == legate_dir - assert c.legate_path == str(legate_dir / "bin" / "legate") - assert c._legate_source == "cmd" - - def test_env_legate_dir_good( - self, monkeypatch: pytest.MonkeyPatch - ) -> None: - legate_dir = Path("/usr/local") - monkeypatch.setenv("LEGATE_DIR", str(legate_dir)) - c = m.Config([]) - assert c.legate_dir == legate_dir - assert c.legate_path == str(legate_dir / "bin" / "legate") - assert c._legate_source == "env" - - def test_extra_args(self) -> None: - extra = ["-foo", "--bar", "--baz", "10"] - c = m.Config(["test.py"] + extra) - assert c.extra_args == extra - - # also test with --files since that option collects arguments - c = m.Config(["test.py", "--files", "a", "b"] + extra) - assert c.extra_args == extra - c = m.Config(["test.py"] + extra + ["--files", "a", "b"]) - assert c.extra_args == extra diff --git a/tests/_utils/tests/test_logger.py b/tests/_utils/tests/test_logger.py deleted file mode 100644 index 637b4a5c7..000000000 --- a/tests/_utils/tests/test_logger.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -from .. import logger as m - -TEST_LINES = ( - "line 1", - "\x1b[31mfoo\x1b[0m", # ui.red("foo") - "bar", - "last line", -) - - -class TestLogger: - def test_init(self) -> None: - log = m.Log() - assert log.lines == () - assert log.dump() == "" - - def test_record_lines(self) -> None: - log = m.Log() - log.record(*TEST_LINES) - assert log.lines == TEST_LINES - assert log.dump(filter_ansi=False) == "\n".join(TEST_LINES) - - def test_record_line_with_newlines(self) -> None: - log = m.Log() - log.record("\n".join(TEST_LINES)) - assert log.lines == TEST_LINES - assert log.dump(filter_ansi=False) == "\n".join(TEST_LINES) - - def test_call(self) -> None: - log = m.Log() - log(*TEST_LINES) - assert log.lines == TEST_LINES - assert log.dump() == "line 1\nfoo\nbar\nlast line" - - def test_dump_filter(self) -> None: - log = m.Log() - log.record(*TEST_LINES) - assert log.lines == TEST_LINES - assert log.dump() == "line 1\nfoo\nbar\nlast line" - - def test_dump_index(self) -> None: - log = m.Log() - log.record(*TEST_LINES) - assert log.dump(start=1, end=3) == "foo\nbar" - - def test_clear(self) -> None: - log = m.Log() - log.record(*TEST_LINES) - assert len(log.lines) > 0 - log.clear() - assert len(log.lines) == 0 - - -def test_LOG() -> None: - assert isinstance(m.LOG, m.Log) diff --git a/tests/_utils/tests/test_system.py b/tests/_utils/tests/test_system.py deleted file mode 100644 index d110e260f..000000000 --- a/tests/_utils/tests/test_system.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -import sys -from pathlib import Path -from subprocess import CompletedProcess -from unittest.mock import MagicMock - -import pytest -from pytest_mock import MockerFixture - -from .. import system as m - - -@pytest.fixture -def mock_subprocess_run(mocker: MockerFixture) -> MagicMock: - return mocker.patch.object(m, "stdlib_run") - - -CMD = "legate script.py --cpus 4" - - -class TestSystem: - def test_init(self) -> None: - s = m.System() - assert s.dry_run is False - - def test_run(self, mock_subprocess_run: MagicMock) -> None: - s = m.System() - - expected = m.ProcessResult( - CMD, Path("test/file"), returncode=10, output="" - ) - mock_subprocess_run.return_value = CompletedProcess( - CMD, 10, stdout="" - ) - - result = s.run(CMD.split(), Path("test/file")) - mock_subprocess_run.assert_called() - - assert result == expected - - def test_dry_run(self, mock_subprocess_run: MagicMock) -> None: - s = m.System(dry_run=True) - - result = s.run(CMD.split(), Path("test/file")) - mock_subprocess_run.assert_not_called() - - assert result.output == "" - assert result.skipped - - def test_cpus(self) -> None: - s = m.System() - cpus = s.cpus - assert len(cpus) > 0 - assert all(len(cpu.ids) > 0 for cpu in cpus) - - @pytest.mark.skipif(sys.platform != "linux", reason="pynvml required") - def test_gpus(self) -> None: - s = m.System() - # can't really assume / test much here - s.gpus diff --git a/tests/_utils/tests/test_types.py b/tests/_utils/tests/test_types.py deleted file mode 100644 index 30fe05a37..000000000 --- a/tests/_utils/tests/test_types.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -from .. import types as m - - -class TestCPUInfo: - def test_fields(self) -> None: - assert set(m.CPUInfo.__dataclass_fields__) == {"ids"} - - -class TestGPUInfo: - def test_fields(self) -> None: - assert set(m.GPUInfo.__dataclass_fields__) == {"id", "total"} diff --git a/tests/_utils/tests/test_ui.py b/tests/_utils/tests/test_ui.py deleted file mode 100644 index 9cc92948a..000000000 --- a/tests/_utils/tests/test_ui.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Consolidate test configuration from command-line and environment. - -""" -from __future__ import annotations - -from datetime import timedelta - -import pytest -from pytest_mock import MockerFixture - -from .. import UI_WIDTH, ui as m - - -@pytest.fixture(autouse=True) -def use_plain_text(mocker: MockerFixture) -> None: - mocker.patch.object(m, "bright", m._text) - mocker.patch.object(m, "dim", m._text) - mocker.patch.object(m, "white", m._text) - mocker.patch.object(m, "cyan", m._text) - mocker.patch.object(m, "red", m._text) - mocker.patch.object(m, "green", m._text) - mocker.patch.object(m, "yellow", m._text) - - -def test_banner_simple() -> None: - assert ( - m.banner("some text") - == "\n" + "#" * UI_WIDTH + "\n### some text\n" + "#" * UI_WIDTH - ) - - -def test_banner_full() -> None: - assert ( - m.banner("some text", char="*", width=100, details=["a", "b"]) - == "\n" - + "*" * 100 - + "\n*** \n*** some text\n*** \n*** a\n*** b\n*** \n" - + "*" * 100 - ) - - -def test_rule_default() -> None: - assert m.rule() == " " + "~" * (UI_WIDTH - 4) - - -def test_rule_with_args() -> None: - assert m.rule(10, "-") == " " * 10 + "-" * (UI_WIDTH - 10) - - -def test_shell() -> None: - assert m.shell("cmd --foo") == "+cmd --foo" - - -def test_shell_with_char() -> None: - assert m.shell("cmd --foo", char="") == "cmd --foo" - - -def test_passed() -> None: - assert m.passed("msg") == "[PASS] msg" - - -def test_passed_with_details() -> None: - assert m.passed("msg", details=["a", "b"]) == "[PASS] msg\n a\n b" - - -def test_failed() -> None: - assert m.failed("msg") == "[FAIL] msg" - - -def test_failed_with_details() -> None: - assert m.failed("msg", details=["a", "b"]) == "[FAIL] msg\n a\n b" - - -def test_skipped() -> None: - assert m.skipped("msg") == "[SKIP] msg" - - -def test_summary() -> None: - assert ( - m.summary("foo", 12, 11, timedelta(seconds=2.123)) - == f"{'foo: Passed 11 of 12 tests (91.7%) in 2.12s': >{UI_WIDTH}}" - ) - - -def test_summary_no_justify() -> None: - assert ( - m.summary("foo", 12, 11, timedelta(seconds=2.123), justify=False) - == "foo: Passed 11 of 12 tests (91.7%) in 2.12s" - ) diff --git a/tests/_utils/types.py b/tests/_utils/types.py deleted file mode 100644 index 1641bd597..000000000 --- a/tests/_utils/types.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Provide types that are useful throughout the test driver code. - -""" -from __future__ import annotations - -from dataclasses import dataclass -from typing import Dict, List - -from typing_extensions import TypeAlias - - -@dataclass(frozen=True) -class CPUInfo: - """Encapsulate information about a single CPU""" - - #: IDs of hypterthreading sibling cores for a given physscal core - ids: tuple[int, ...] - - -@dataclass(frozen=True) -class GPUInfo: - """Encapsulate information about a single CPU""" - - #: ID of the GPU to specify in test shards - id: int - - #: The total framebuffer memory of this GPU - total: int - - -#: Represent command line arguments -ArgList = List[str] - - -#: Represent str->str environment variable mappings -EnvDict: TypeAlias = Dict[str, str] diff --git a/tests/_utils/ui.py b/tests/_utils/ui.py deleted file mode 100644 index eaa97d7c0..000000000 --- a/tests/_utils/ui.py +++ /dev/null @@ -1,229 +0,0 @@ -# Copyright AS2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Helpler functions for simple text UI output. - -The color functions in this module require ``colorama`` to be installed in -order to generate color output. If ``colorama`` is not available, plain -text output (i.e. without ANSI color codes) will generated. - -""" -from __future__ import annotations - -import sys -from datetime import timedelta -from typing import Iterable - -from typing_extensions import TypeAlias - -from . import UI_WIDTH - -Details: TypeAlias = Iterable[str] - - -def _text(text: str) -> str: - return text - - -try: - import colorama # type: ignore[import] - - def bright(text: str) -> str: - return f"{colorama.Style.BRIGHT}{text}{colorama.Style.RESET_ALL}" - - def dim(text: str) -> str: - return f"{colorama.Style.DIM}{text}{colorama.Style.RESET_ALL}" - - def white(text: str) -> str: - return f"{colorama.Fore.WHITE}{text}{colorama.Style.RESET_ALL}" - - def cyan(text: str) -> str: - return f"{colorama.Fore.CYAN}{text}{colorama.Style.RESET_ALL}" - - def red(text: str) -> str: - return f"{colorama.Fore.RED}{text}{colorama.Style.RESET_ALL}" - - def green(text: str) -> str: - return f"{colorama.Fore.GREEN}{text}{colorama.Style.RESET_ALL}" - - def yellow(text: str) -> str: - return f"{colorama.Fore.YELLOW}{text}{colorama.Style.RESET_ALL}" - - if sys.platform == "win32": - colorama.init() - -except ImportError: - - bright = dim = white = cyan = red = green = yellow = _text - - -def _format_details( - details: Iterable[str] | None = None, pre: str = " " -) -> str: - if details: - return f"{pre}" + f"\n{pre}".join(f"{line}" for line in details) - return "" - - -def banner( - heading: str, - *, - char: str = "#", - width: int = UI_WIDTH, - details: Iterable[str] | None = None, -) -> str: - """Generate a title banner, with optional details included. - - Parameters - ---------- - heading : str - Text to use for the title - - char : str, optional - A character to use to frame the banner. (default: "#") - - width : int, optional - How wide to draw the banner. (Note: user-supplied heading or - details willnot be truncated if they exceed this width) - - details : Iterable[str], optional - A list of lines to diplay inside the banner area below the heading - - """ - pre = f"{char*3} " - divider = char * width - if not details: - return f"\n{divider}\n{pre}{heading}\n{divider}" - return f""" -{divider} -{pre} -{pre}{heading} -{pre} -{_format_details(details, pre)} -{pre} -{divider}""" - - -def failed(msg: str, *, details: Details | None = None) -> str: - """Report a failed test result with a bright red [FAIL]. - - Parameters - ---------- - msg : str - Text to display after [FAIL] - - details : Iterable[str], optional - A sequenece of text lines to diplay below the ``msg`` line - - """ - if details: - return f"{bright(red('[FAIL]'))} {msg}\n{_format_details(details)}" - return f"{bright(red('[FAIL]'))} {msg}" - - -def passed(msg: str, *, details: Details | None = None) -> str: - """Report a passed test result with a bright green [PASS]. - - Parameters - ---------- - msg : str - Text to display after [PASS] - - details : Iterable[str], optional - A sequenece of text lines to diplay below the ``msg`` line - - """ - if details: - return f"{bright(green('[PASS]'))} {msg}\n{_format_details(details)}" - return f"{bright(green('[PASS]'))} {msg}" - - -def rule(pad: int = 4, char: str = "~") -> str: - """Generate a horizontal rule. - - Parameters - ---------- - pad : int, optional - How much whitespace to precede the rule. (default: 4) - - char : str, optional - A character to use to "draw" the rule. (default: "~") - - """ - w = UI_WIDTH - pad - return f"{char*w: >{UI_WIDTH}}" - - -def shell(cmd: str, *, char: str = "+") -> str: - """Report a shell command in a dim white color. - - Parameters - ---------- - cmd : str - The shell command string to display - - char : str, optional - A character to prefix the ``cmd`` with. (default: "+") - - """ - return dim(white(f"{char}{cmd}")) - - -def skipped(msg: str) -> str: - """Report a skipped test with a cyan [SKIP] - - Parameters - ---------- - msg : str - Text to display after [SKIP] - - """ - return f"{cyan('[SKIP]')} {msg}" - - -def summary( - name: str, - total: int, - passed: int, - time: timedelta, - *, - justify: bool = True, -) -> str: - """Generate a test result summary line. - - The output is bright green if all tests passed, otherwise bright red. - - Parameters - ---------- - name : str - A name to display in this summary line. - - total : int - The total number of tests to report. - - passed : int - The number of passed tests to report. - - time : timedelta - The time taken to run the tests - - """ - summary = ( - f"{name}: Passed {passed} of {total} tests ({passed/total*100:0.1f}%) " - f"in {time.total_seconds():0.2f}s" - if total > 0 - else f"{name}: 0 tests are running, Please check" - ) - color = green if passed == total and total > 0 else red - return bright(color(f"{summary: >{UI_WIDTH}}" if justify else summary))