1012 lines
35 KiB
Python
Executable File
1012 lines
35 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Summarize (and optionally run) the LAPACK Fortran test suite.
|
|
|
|
This script analyzes the ``.out`` files written by the LAPACK testing
|
|
drivers (``xlintst*``, ``xeigtst*`` and ``xdmdeigtst*``) and prints a
|
|
summary table of the number of tests run and the number of failures per
|
|
precision (s/d/c/z). With ``--run`` it executes the testing drivers
|
|
first and then analyzes their output.
|
|
|
|
When index-64 extended API outputs (``*_64.out``, produced by CMake
|
|
builds with ``BUILD_INDEX64_EXT_API=ON``) are present, they are analyzed
|
|
as well and reported in a separate "extended API" section so that the
|
|
default-API totals remain comparable across builds.
|
|
|
|
Examples:
|
|
./lapack_testing.py -n
|
|
Print the numbers of failed tests by analyzing the LAPACK output.
|
|
|
|
./lapack_testing.py -n -r -p s
|
|
Run the REAL precision tests, then print the numbers of failures.
|
|
|
|
./lapack_testing.py -n -p s -t eig
|
|
Print the numbers of failures in REAL precision by analyzing only
|
|
the eigenproblem test output.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import io
|
|
import math
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Dict, List, Optional, Sequence, TextIO, Tuple
|
|
|
|
# Precision letters and the labels used in the summary table.
|
|
PRECISIONS: "Tuple[Tuple[str, str], ...]" = (
|
|
("s", "REAL"),
|
|
("d", "DOUBLE PRECISION"),
|
|
("c", "COMPLEX"),
|
|
("z", "COMPLEX16"),
|
|
)
|
|
|
|
# Second precision letter of the mixed-precision linear equation tests.
|
|
MIXED_PARTNER: "Dict[str, str]" = {"d": "s", "z": "c"}
|
|
|
|
# Eigenproblem test sets using the classic ``alasum``/``alasvm`` summary
|
|
# format: (name, has shared input file, description). Sets with a shared
|
|
# input read e.g. ``nep.in``; the others read e.g. ``sec.in``/``dec.in``.
|
|
EIG_STANDARD_SETS: "Tuple[Tuple[str, bool, str], ...]" = (
|
|
("nep", True, "Nonsymmetric Eigenvalue Problem"),
|
|
("sep", True, "Symmetric Eigenvalue Problem"),
|
|
("se2", True, "Symmetric Eigenvalue Problem 2-stage"),
|
|
("svd", True, "Singular Value Decomposition"),
|
|
("ec", False, "Eigen Condition"),
|
|
("ed", False, "Nonsymmetric Eigenvalue"),
|
|
("gg", False, "Nonsymmetric Generalized Eigenvalue Problem"),
|
|
("gd", False, "Nonsymmetric Generalized Eigenvalue Problem driver"),
|
|
("sb", False, "Symmetric Eigenvalue Problem"),
|
|
("sg", False, "Symmetric Eigenvalue Generalized Problem"),
|
|
("bb", False, "Banded Singular Value Decomposition routines"),
|
|
("glm", True, "Generalized Linear Regression Model routines"),
|
|
("gqr", True, "Generalized QR and RQ factorization routines"),
|
|
("gsv", True, "Generalized Singular Value Decomposition routines"),
|
|
("csd", True, "CS Decomposition routines"),
|
|
("lse", True, "Constrained Linear Least Squares routines"),
|
|
)
|
|
|
|
# Balancing/backtransformation test sets, which use the ``schkbl``-style
|
|
# "total number of examples tested" summary format.
|
|
EIG_BALANCE_SETS: "Tuple[Tuple[str, str], ...]" = (
|
|
("bal", "Matrix Balancing"),
|
|
("bak", "Balancing Backtransformation"),
|
|
("gbal", "Generalized Matrix Balancing"),
|
|
("gbak", "Generalized Balancing Backtransformation"),
|
|
)
|
|
|
|
# Linear equation test sets: (family, name stem, executable prefix,
|
|
# description). Names and executables are built from the precision
|
|
# letter plus the stem, e.g. stest.in/stest.out/xlintsts; the mixed
|
|
# precision sets use the letter of both precisions (dstest, xlintstds)
|
|
# and exist only for the precisions in MIXED_PARTNER.
|
|
LIN_SETS: "Tuple[Tuple[str, str, str, str], ...]" = (
|
|
("lin", "test", "xlintst", "Linear Equation routines"),
|
|
("mixed", "test", "xlintst", "Mixed Precision linear equation routines"),
|
|
("rfp", "test_rfp", "xlintstrf", "RFP linear equation routines"),
|
|
)
|
|
|
|
# All test families, in reporting order per precision.
|
|
ALL_FAMILIES: "Tuple[str, ...]" = ("eig",) + tuple(s[0] for s in LIN_SETS) + ("dmd",)
|
|
|
|
# API suffixes that may exist: default API and index-64 extended API.
|
|
KNOWN_SUFFIXES: "Tuple[str, ...]" = ("", "_64")
|
|
|
|
RESULTS_FILENAME = "testing_results.txt"
|
|
|
|
# Classic summary lines printed by alasum.f/alasvm.f:
|
|
# " All tests for XYZ routines passed the threshold ( ddd tests run)"
|
|
# " XYZ: ddd out of ddd tests failed to pass the threshold"
|
|
RE_TESTS_RUN = re.compile(r"(\d+)\s+tests run\)")
|
|
RE_TESTS_FAILED = re.compile(r"(\d+)\s+out of\s+(\d+)")
|
|
|
|
# Failure records printed by the eigencondition checkers (schkec.f and
|
|
# friends), e.g. " Error in STRSYL: RMAX =..." — one per failing routine.
|
|
RE_EC_ERROR = re.compile(r"^ ?Error in \w+")
|
|
|
|
# Summary lines printed by the balancing checkers (schkbl.f and friends).
|
|
# The complex generalized checkers use slightly different wording
|
|
# ("ratio of largest test error", "ILO or IHI is wrong").
|
|
RE_EXAMPLES_TESTED = re.compile(r"total number of examples tested\s*=\s*(\d+)")
|
|
RE_INFO_NOT_ZERO = re.compile(r"number of examples where info is not 0\s*=\s*(\d+)")
|
|
RE_ILO_IHI_WRONG = re.compile(
|
|
r"example number where ILO or IHI (?:is )?wrong\s*=\s*(\d+)"
|
|
)
|
|
RE_LARGEST_ERROR = re.compile(r"(?:value|ratio) of largest test error\s*=\s*(\S+)")
|
|
|
|
# Per-test verdict lines printed by the DMD checkers (schkdmd.f90 and
|
|
# friends), e.g. ">>>> Z - U*V test PASSED.". The word boundary keeps
|
|
# aggregate lines such as "SGEDMD :: ALL TESTS PASSED." from matching.
|
|
RE_DMD_VERDICT = re.compile(r"\btest\s+(PASSED|FAILED)\b", re.IGNORECASE)
|
|
|
|
# Parser kinds, used by TestCase.parser.
|
|
PARSER_STANDARD = "standard"
|
|
PARSER_BALANCE = "balance"
|
|
PARSER_DMD = "dmd"
|
|
|
|
|
|
@dataclass
|
|
class Counts:
|
|
"""Accumulated test counts for one or more test output files."""
|
|
|
|
runs: int = 0
|
|
numerical: int = 0
|
|
illegal: int = 0
|
|
info: int = 0
|
|
|
|
@property
|
|
def other(self) -> int:
|
|
"""Return the number of non-numerical errors (illegal + info).
|
|
|
|
Returns:
|
|
The combined number of "illegal value" and INFO errors.
|
|
"""
|
|
return self.illegal + self.info
|
|
|
|
@property
|
|
def errors(self) -> int:
|
|
"""Return the total number of errors of any kind.
|
|
|
|
Returns:
|
|
The combined number of numerical and other errors.
|
|
"""
|
|
return self.numerical + self.other
|
|
|
|
def add(self, other: "Counts") -> None:
|
|
"""Accumulate another set of counts into this one.
|
|
|
|
Args:
|
|
other: The counts to add in place.
|
|
"""
|
|
self.runs += other.runs
|
|
self.numerical += other.numerical
|
|
self.illegal += other.illegal
|
|
self.info += other.info
|
|
|
|
|
|
@dataclass
|
|
class FileReport:
|
|
"""Parsing result for a single test output file."""
|
|
|
|
counts: Counts = field(default_factory=Counts)
|
|
notable_lines: "List[str]" = field(default_factory=list)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TestCase:
|
|
"""One LAPACK test driver invocation and its expected output file."""
|
|
|
|
precision: str
|
|
family: str
|
|
description: str
|
|
input_name: str
|
|
output_name: str
|
|
executable: str
|
|
parser: str
|
|
|
|
def suffixed_output(self, suffix: str) -> str:
|
|
"""Return the output file name for an API suffix.
|
|
|
|
Args:
|
|
suffix: The API suffix, either ``""`` or ``"_64"``.
|
|
|
|
Returns:
|
|
The output file name, e.g. ``snep_64.out`` for suffix
|
|
``"_64"`` and base output name ``snep.out``.
|
|
"""
|
|
stem = self.output_name[: -len(".out")]
|
|
return "{}{}.out".format(stem, suffix)
|
|
|
|
def suffixed_executable(self, suffix: str) -> str:
|
|
"""Return the test driver name for an API suffix.
|
|
|
|
Args:
|
|
suffix: The API suffix, either ``""`` or ``"_64"``.
|
|
|
|
Returns:
|
|
The executable name, e.g. ``xeigtsts_64``.
|
|
"""
|
|
return self.executable + suffix
|
|
|
|
|
|
def build_test_cases(letters: str, families: "Sequence[str]") -> "List[TestCase]":
|
|
"""Build the list of test cases for the selected precisions/families.
|
|
|
|
Args:
|
|
letters: Precision letters to include, in order (subset of
|
|
``"sdcz"``).
|
|
families: Test families to include (subset of ``lin``, ``eig``,
|
|
``mixed``, ``rfp``, ``dmd``).
|
|
|
|
Returns:
|
|
The test cases in reporting order: for each precision, the
|
|
eigenproblem sets, then the linear equation, mixed precision,
|
|
RFP and DMD sets.
|
|
"""
|
|
cases: "List[TestCase]" = []
|
|
for letter in letters:
|
|
if "eig" in families:
|
|
for name, shared_input, description in EIG_STANDARD_SETS:
|
|
cases.append(
|
|
TestCase(
|
|
precision=letter,
|
|
family="eig",
|
|
description=description,
|
|
input_name=(name if shared_input else letter + name) + ".in",
|
|
output_name=letter + name + ".out",
|
|
executable="xeigtst" + letter,
|
|
parser=PARSER_STANDARD,
|
|
)
|
|
)
|
|
for name, description in EIG_BALANCE_SETS:
|
|
cases.append(
|
|
TestCase(
|
|
precision=letter,
|
|
family="eig",
|
|
description=description,
|
|
input_name=letter + name + ".in",
|
|
output_name=letter + name + ".out",
|
|
executable="xeigtst" + letter,
|
|
parser=PARSER_BALANCE,
|
|
)
|
|
)
|
|
for family, stem, executable_prefix, description in LIN_SETS:
|
|
if family not in families:
|
|
continue
|
|
if family == "mixed":
|
|
if letter not in MIXED_PARTNER:
|
|
continue
|
|
letters_part = letter + MIXED_PARTNER[letter]
|
|
else:
|
|
letters_part = letter
|
|
cases.append(
|
|
TestCase(
|
|
precision=letter,
|
|
family=family,
|
|
description=description,
|
|
input_name=letters_part + stem + ".in",
|
|
output_name=letters_part + stem + ".out",
|
|
executable=executable_prefix + letters_part,
|
|
parser=PARSER_STANDARD,
|
|
)
|
|
)
|
|
if "dmd" in families:
|
|
cases.append(
|
|
TestCase(
|
|
precision=letter,
|
|
family="dmd",
|
|
description="Dynamic Mode Decomposition",
|
|
input_name=letter + "dmd.in",
|
|
output_name=letter + "dmd.out",
|
|
executable="xdmdeigtst" + letter,
|
|
parser=PARSER_DMD,
|
|
)
|
|
)
|
|
return cases
|
|
|
|
|
|
def parse_standard(lines: "Sequence[str]") -> FileReport:
|
|
"""Parse a test output file in the classic alasum/alasvm format.
|
|
|
|
Counts runs from both the passing summary lines (``... tests run)``)
|
|
and the failing summary lines (``N out of M tests failed ...``), so
|
|
that failing test sets contribute to the run total as well. The
|
|
eigencondition checkers report failures as ``Error in <routine>``
|
|
records instead; each such record counts as one numerical failure.
|
|
|
|
Args:
|
|
lines: The lines of the output file.
|
|
|
|
Returns:
|
|
The counts and the notable (error) lines of the file.
|
|
"""
|
|
report = FileReport()
|
|
for line in lines:
|
|
match = RE_TESTS_RUN.search(line)
|
|
if match:
|
|
report.counts.runs += int(match.group(1))
|
|
continue
|
|
match = RE_TESTS_FAILED.search(line)
|
|
if match:
|
|
report.counts.numerical += int(match.group(1))
|
|
report.counts.runs += int(match.group(2))
|
|
report.notable_lines.append(line)
|
|
continue
|
|
if RE_EC_ERROR.match(line):
|
|
report.counts.numerical += 1
|
|
report.notable_lines.append(line)
|
|
continue
|
|
if "illegal" in line or "Illegal" in line:
|
|
report.counts.illegal += 1
|
|
report.notable_lines.append(line)
|
|
continue
|
|
if " INFO" in line:
|
|
report.counts.info += 1
|
|
report.notable_lines.append(line)
|
|
return report
|
|
|
|
|
|
def parse_balance(lines: "Sequence[str]") -> FileReport:
|
|
"""Parse a balancing/backtransformation test output file.
|
|
|
|
These checkers (``schkbl.f`` and friends) do not use the alasum
|
|
summary format. Runs are taken from the ``total number of examples
|
|
tested`` line, INFO errors from the ``number of examples where info
|
|
is not 0`` line. A non-finite ``value of largest test error`` or a
|
|
nonzero ``example number where ILO or IHI wrong`` is counted as one
|
|
numerical failure.
|
|
|
|
Args:
|
|
lines: The lines of the output file.
|
|
|
|
Returns:
|
|
The counts and the notable (error) lines of the file.
|
|
"""
|
|
report = FileReport()
|
|
for line in lines:
|
|
match = RE_EXAMPLES_TESTED.search(line)
|
|
if match:
|
|
report.counts.runs += int(match.group(1))
|
|
continue
|
|
match = RE_INFO_NOT_ZERO.search(line)
|
|
if match:
|
|
info_errors = int(match.group(1))
|
|
report.counts.info += info_errors
|
|
if info_errors > 0:
|
|
report.notable_lines.append(line)
|
|
continue
|
|
match = RE_ILO_IHI_WRONG.search(line)
|
|
if match:
|
|
if int(match.group(1)) != 0:
|
|
report.counts.numerical += 1
|
|
report.notable_lines.append(line)
|
|
continue
|
|
match = RE_LARGEST_ERROR.search(line)
|
|
if match:
|
|
# Fortran prints double precision exponents as 0.1D+01.
|
|
token = match.group(1).replace("D", "E").replace("d", "e")
|
|
try:
|
|
value = float(token)
|
|
except ValueError:
|
|
value = math.inf
|
|
if not math.isfinite(value):
|
|
report.counts.numerical += 1
|
|
report.notable_lines.append(line)
|
|
return report
|
|
|
|
|
|
def parse_dmd(lines: "Sequence[str]") -> FileReport:
|
|
"""Parse a dynamic mode decomposition test output file.
|
|
|
|
Each per-test verdict line (``... test PASSED.`` or ``... test
|
|
FAILED ...``) counts as one test run; each FAILED verdict counts as
|
|
one numerical failure (the line itself reports how many individual
|
|
cases failed).
|
|
|
|
Args:
|
|
lines: The lines of the output file.
|
|
|
|
Returns:
|
|
The counts and the notable (error) lines of the file.
|
|
"""
|
|
report = FileReport()
|
|
for line in lines:
|
|
match = RE_DMD_VERDICT.search(line)
|
|
if match:
|
|
report.counts.runs += 1
|
|
if match.group(1).upper() == "FAILED":
|
|
report.counts.numerical += 1
|
|
report.notable_lines.append(line)
|
|
return report
|
|
|
|
|
|
def parse_lines(parser: str, lines: "Sequence[str]") -> FileReport:
|
|
"""Parse test output lines with the parser kind of a test case.
|
|
|
|
Args:
|
|
parser: One of ``PARSER_STANDARD``, ``PARSER_BALANCE`` and
|
|
``PARSER_DMD``.
|
|
lines: The lines of the output file.
|
|
|
|
Returns:
|
|
The counts and the notable (error) lines of the file.
|
|
"""
|
|
if parser == PARSER_BALANCE:
|
|
return parse_balance(lines)
|
|
if parser == PARSER_DMD:
|
|
return parse_dmd(lines)
|
|
return parse_standard(lines)
|
|
|
|
|
|
def find_unrecognized_outputs(test_dir: Path) -> "List[str]":
|
|
"""Find ``.out`` files in the test directory this script cannot analyze.
|
|
|
|
A file is unrecognized if its name matches no known test case in any
|
|
precision, family or API variant — typically a test that was added to
|
|
the harness without extending this script's test tables, or a renamed
|
|
output such as those of ``make variants_testing``. The current
|
|
``-p``/``-t`` selection is deliberately ignored: a deselected file is
|
|
not an unrecognized one.
|
|
|
|
Args:
|
|
test_dir: The directory containing the ``.out`` files.
|
|
|
|
Returns:
|
|
The unrecognized file names, sorted alphabetically.
|
|
"""
|
|
all_cases = build_test_cases("sdcz", ALL_FAMILIES)
|
|
known = {
|
|
case.suffixed_output(suffix) for case in all_cases for suffix in KNOWN_SUFFIXES
|
|
}
|
|
return sorted(
|
|
path.name for path in test_dir.glob("*.out") if path.name not in known
|
|
)
|
|
|
|
|
|
def discover_suffixes(test_dir: Path, cases: "Sequence[TestCase]") -> "List[str]":
|
|
"""Detect which API variants have output files in the test directory.
|
|
|
|
Args:
|
|
test_dir: The directory containing the ``.out`` files.
|
|
cases: The selected test cases.
|
|
|
|
Returns:
|
|
The suffixes (out of ``""`` and ``"_64"``) for which at least one
|
|
expected output file exists; ``[""]`` if none exist at all.
|
|
"""
|
|
suffixes = [
|
|
suffix
|
|
for suffix in KNOWN_SUFFIXES
|
|
if any((test_dir / case.suffixed_output(suffix)).is_file() for case in cases)
|
|
]
|
|
return suffixes or [""]
|
|
|
|
|
|
def find_executable(name: str, bin_dir: "Optional[str]") -> "Optional[Path]":
|
|
"""Locate a test driver executable.
|
|
|
|
Args:
|
|
name: The executable name without platform suffix, e.g.
|
|
``xlintsts``.
|
|
bin_dir: The directory passed via ``--bin``, or None to probe the
|
|
usual locations of CMake and Makefile builds relative to the
|
|
current working directory.
|
|
|
|
Returns:
|
|
The absolute path of the executable, or None if it was not found.
|
|
"""
|
|
if bin_dir is not None:
|
|
directories = [Path(bin_dir)]
|
|
else:
|
|
directories = [
|
|
Path("bin"),
|
|
Path("bin") / "Release",
|
|
Path("bin") / "Debug",
|
|
Path("TESTING") / "LIN",
|
|
Path("TESTING") / "EIG",
|
|
]
|
|
for directory in directories:
|
|
for filename in (name, name + ".exe"):
|
|
candidate = directory / filename
|
|
if candidate.is_file():
|
|
return candidate.resolve()
|
|
return None
|
|
|
|
|
|
def run_test_case(
|
|
case: TestCase, suffix: str, test_dir: Path, bin_dir: "Optional[str]"
|
|
) -> "Optional[str]":
|
|
"""Run one test driver, redirecting its output to the ``.out`` file.
|
|
|
|
Args:
|
|
case: The test case to run.
|
|
suffix: The API suffix, either ``""`` or ``"_64"``.
|
|
test_dir: The directory containing the ``.in`` files; the driver
|
|
runs there and the ``.out`` file is written there.
|
|
bin_dir: The directory containing the test drivers, or None to
|
|
probe the usual locations.
|
|
|
|
Returns:
|
|
An error message if the driver could not be run or exited with a
|
|
nonzero status, otherwise None.
|
|
"""
|
|
executable_name = case.suffixed_executable(suffix)
|
|
executable = find_executable(executable_name, bin_dir)
|
|
if executable is None:
|
|
return "executable {} not found".format(executable_name)
|
|
input_path = test_dir / case.input_name
|
|
if not input_path.is_file():
|
|
# CMake build trees hold only the .out files; the .in files
|
|
# live next to this script in the source tree.
|
|
source_input = Path(__file__).resolve().parent / "TESTING" / case.input_name
|
|
if source_input.is_file():
|
|
input_path = source_input
|
|
else:
|
|
return "input file {} not found".format(input_path)
|
|
output_path = test_dir / case.suffixed_output(suffix)
|
|
# Write to a temporary file first so that a driver that cannot even
|
|
# start does not clobber the results of an earlier run.
|
|
temporary_path = output_path.with_name(output_path.name + ".tmp")
|
|
try:
|
|
with open(str(input_path), "rb") as stdin, open(
|
|
str(temporary_path), "wb"
|
|
) as stdout:
|
|
process = subprocess.run(
|
|
[str(executable)],
|
|
stdin=stdin,
|
|
stdout=stdout,
|
|
stderr=subprocess.STDOUT,
|
|
cwd=str(test_dir),
|
|
)
|
|
except OSError as error:
|
|
try:
|
|
temporary_path.unlink()
|
|
except OSError:
|
|
pass
|
|
return "{} could not be run: {}".format(executable_name, error)
|
|
temporary_path.replace(output_path)
|
|
if process.returncode != 0:
|
|
return "{} exited with status {}".format(executable_name, process.returncode)
|
|
return None
|
|
|
|
|
|
def read_output_file(path: Path) -> "Optional[List[str]]":
|
|
"""Read a test output file.
|
|
|
|
Args:
|
|
path: The path of the ``.out`` file.
|
|
|
|
Returns:
|
|
The lines of the file, or None if the file does not exist or
|
|
cannot be read (the cause is then reported on standard error).
|
|
"""
|
|
if not path.is_file():
|
|
return None
|
|
try:
|
|
with open(str(path), encoding="utf-8", errors="replace") as handle:
|
|
return handle.readlines()
|
|
except OSError as error:
|
|
print(
|
|
"lapack_testing.py: cannot read {}: {}".format(path, error),
|
|
file=sys.stderr,
|
|
)
|
|
return None
|
|
|
|
|
|
# Fixed-width summary table columns: label, run count and one block of
|
|
# "count (percent)" per error kind. Values are right-aligned so that
|
|
# they line up under the ==== rule of their column. The 73-column
|
|
# table is indented to sit centered under the 80-column headings.
|
|
SUMMARY_INDENT = " "
|
|
SUMMARY_HEADER = SUMMARY_INDENT + "{:<18} {:>13} {:>18} {:>18}".format(
|
|
"SUMMARY", "nb test run", "numerical error", "other error"
|
|
)
|
|
SUMMARY_RULE = SUMMARY_INDENT + " ".join(("=" * 18, "=" * 13, "=" * 18, "=" * 18))
|
|
|
|
|
|
def format_summary_row(label: str, counts: Counts) -> str:
|
|
"""Format one row of the summary table.
|
|
|
|
Args:
|
|
label: The row label, e.g. a precision name.
|
|
counts: The counts to report in the row.
|
|
|
|
Returns:
|
|
The formatted table row without trailing newline.
|
|
"""
|
|
if counts.runs > 0:
|
|
numerical_percent = 100.0 * counts.numerical / counts.runs
|
|
other_percent = 100.0 * counts.other / counts.runs
|
|
else:
|
|
numerical_percent = 0.0
|
|
other_percent = 0.0
|
|
numerical_percent_str = "({:.1f}%)".format(numerical_percent)
|
|
other_percent_str = "({:.1f}%)".format(other_percent)
|
|
return SUMMARY_INDENT + "{:<18} {:>13} {:>9} {:>8} {:>9} {:>8}".format(
|
|
label,
|
|
counts.runs,
|
|
counts.numerical,
|
|
numerical_percent_str,
|
|
counts.other,
|
|
other_percent_str,
|
|
)
|
|
|
|
|
|
def section_title(suffix: str) -> str:
|
|
"""Return the human-readable name of an API section.
|
|
|
|
Args:
|
|
suffix: The API suffix, either ``""`` or ``"_64"``.
|
|
|
|
Returns:
|
|
The section name used in headings and messages.
|
|
"""
|
|
if suffix:
|
|
return "Extended API ({})".format(suffix)
|
|
return "Default API"
|
|
|
|
|
|
def section_heading(title: str) -> str:
|
|
"""Return an 80-column dashed heading with a centered title.
|
|
|
|
Args:
|
|
title: The heading text.
|
|
|
|
Returns:
|
|
str: A line of exactly 80 characters — dashes running to both
|
|
edges (one space at the beginning and end) with the title
|
|
centered.
|
|
"""
|
|
return " {} \n".format(" {} ".format(title).center(78, "-"))
|
|
|
|
|
|
class SummaryLog:
|
|
"""Collector for the detailed results file (``testing_results.txt``)."""
|
|
|
|
def __init__(self, handle: "Optional[TextIO]") -> None:
|
|
"""Initialize the collector.
|
|
|
|
Args:
|
|
handle: The open results file, or None if it could not be
|
|
opened (details are then discarded).
|
|
"""
|
|
self._handle = handle
|
|
|
|
def record(self, header: str, lines: "Sequence[str]") -> None:
|
|
"""Append one analyzed output file to the results file.
|
|
|
|
Args:
|
|
header: A short description of the file (its name).
|
|
lines: The lines of the file.
|
|
"""
|
|
if self._handle is None:
|
|
return
|
|
self._handle.write("==== {} ====\n".format(header))
|
|
self._handle.writelines(lines)
|
|
self._handle.flush()
|
|
|
|
def close(self) -> None:
|
|
"""Close the results file if it was open."""
|
|
if self._handle is not None:
|
|
self._handle.close()
|
|
|
|
|
|
def parse_args(argv: "Optional[Sequence[str]]" = None) -> argparse.Namespace:
|
|
"""Parse the command line arguments.
|
|
|
|
Args:
|
|
argv: The arguments to parse, or None to use ``sys.argv``.
|
|
|
|
Returns:
|
|
The parsed arguments.
|
|
"""
|
|
parser = argparse.ArgumentParser(
|
|
description="Analyze the .out files produced by the LAPACK test "
|
|
"suite and print a summary of the test results.",
|
|
epilog="By default all precisions and all test families are "
|
|
"analyzed, and both the default API and extended API (_64) "
|
|
"outputs are summarized when present.",
|
|
)
|
|
parser.add_argument(
|
|
"-d",
|
|
"--dir",
|
|
default="TESTING",
|
|
help="directory containing the LAPACK testing output (.out) files "
|
|
"(default: %(default)s)",
|
|
)
|
|
parser.add_argument(
|
|
"-b",
|
|
"--bin",
|
|
default=None,
|
|
help="directory containing the LAPACK test drivers for --run; by "
|
|
"default bin, bin/Release, bin/Debug, TESTING/LIN and TESTING/EIG "
|
|
"are probed",
|
|
)
|
|
parser.add_argument(
|
|
"-r",
|
|
"--run",
|
|
action="store_true",
|
|
help="run the LAPACK test drivers before analyzing their output "
|
|
"(by default only existing .out files are analyzed)",
|
|
)
|
|
parser.add_argument(
|
|
"-s",
|
|
"--short",
|
|
action="store_true",
|
|
help="print only the summary table",
|
|
)
|
|
parser.add_argument(
|
|
"-e",
|
|
"--error",
|
|
action="store_true",
|
|
help="print only the error summary",
|
|
)
|
|
parser.add_argument(
|
|
"-n",
|
|
"--number",
|
|
action="store_true",
|
|
help="print only the numbers of failing tests (numerical failures "
|
|
"and other errors, one per line)",
|
|
)
|
|
parser.add_argument(
|
|
"-p",
|
|
"--prec",
|
|
choices=["s", "d", "sd", "c", "z", "cz", "x"],
|
|
default="x",
|
|
help="precisions to analyze: s=single, d=double, sd=single/double, "
|
|
"c=complex, z=double complex, cz=complex/double complex, "
|
|
"x=all (default)",
|
|
)
|
|
parser.add_argument(
|
|
"-t",
|
|
"--test",
|
|
choices=list(ALL_FAMILIES) + ["all"],
|
|
default="all",
|
|
help="test family to analyze: lin=linear equations, "
|
|
"eig=eigenproblems (including balancing), mixed=mixed precision, "
|
|
"rfp=RFP format, dmd=dynamic mode decomposition, all (default)",
|
|
)
|
|
parser.add_argument(
|
|
"--suffix",
|
|
action="append",
|
|
choices=["none", "64"],
|
|
default=None,
|
|
help="API variant to analyze: 'none' for the default API, '64' for "
|
|
"the index-64 extended API; may be given twice (default: analyze "
|
|
"whichever variants have output files)",
|
|
)
|
|
parser.add_argument(
|
|
"--fail-on-error",
|
|
action="store_true",
|
|
help="exit with a nonzero status if any test failure or error was "
|
|
"found, a test driver could not be run, or unrecognized .out files "
|
|
"were present in the testing directory",
|
|
)
|
|
parser.add_argument(
|
|
"--fail-if-empty",
|
|
action="store_true",
|
|
help="exit with a nonzero status if no test results were analyzed",
|
|
)
|
|
parser.add_argument(
|
|
"--fail-on-unrecognized",
|
|
action="store_true",
|
|
help="exit with a nonzero status if .out files not known to this "
|
|
"script were present in the testing directory",
|
|
)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def main(argv: "Optional[Sequence[str]]" = None) -> int:
|
|
"""Run the LAPACK test summary tool.
|
|
|
|
Args:
|
|
argv: The command line arguments, or None to use ``sys.argv``.
|
|
|
|
Returns:
|
|
int: The process exit status. This is 2 for usage errors, 1 if a
|
|
condition requested via ``--fail-on-error``, ``--fail-if-empty``
|
|
or ``--fail-on-unrecognized`` occurred, and 0 otherwise.
|
|
"""
|
|
args = parse_args(argv)
|
|
short_summary: bool = args.short or args.number
|
|
just_errors: bool = args.error
|
|
|
|
test_dir = Path(args.dir)
|
|
if not test_dir.is_dir():
|
|
print(
|
|
"lapack_testing.py: testing directory {} not found".format(test_dir),
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
|
|
letters = "sdcz" if args.prec == "x" else args.prec
|
|
if args.test == "mixed":
|
|
# The mixed-precision drivers exist only for d (ds) and z (zc);
|
|
# like the old script, -t mixed analyzes both regardless of -p.
|
|
if args.prec not in ("x", "dz"):
|
|
print(
|
|
"lapack_testing.py: -t mixed always analyzes the d and z "
|
|
"mixed-precision tests; ignoring -p {}".format(args.prec),
|
|
file=sys.stderr,
|
|
)
|
|
letters = "dz"
|
|
families = list(ALL_FAMILIES) if args.test == "all" else [args.test]
|
|
cases = build_test_cases(letters, families)
|
|
if not cases:
|
|
print(
|
|
"lapack_testing.py: no test cases match -p {} -t {}".format(
|
|
args.prec, args.test
|
|
),
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
|
|
if args.suffix is not None:
|
|
suffixes: "List[str]" = []
|
|
for choice in args.suffix:
|
|
suffix = "" if choice == "none" else "_64"
|
|
if suffix not in suffixes:
|
|
suffixes.append(suffix)
|
|
elif args.run:
|
|
suffixes = [""]
|
|
else:
|
|
suffixes = discover_suffixes(test_dir, cases)
|
|
|
|
results_path = test_dir / RESULTS_FILENAME
|
|
try:
|
|
results_handle: "Optional[TextIO]" = open(
|
|
str(results_path), "w", encoding="utf-8"
|
|
)
|
|
except OSError as error:
|
|
print(
|
|
"lapack_testing.py: cannot write {}: {}".format(results_path, error),
|
|
file=sys.stderr,
|
|
)
|
|
results_handle = None
|
|
log = SummaryLog(results_handle)
|
|
|
|
if not short_summary:
|
|
print(" ")
|
|
print("--> Testing LAPACK Routines <--".center(80).rstrip())
|
|
print(" ")
|
|
print("Detailed results are stored in:".center(80).rstrip())
|
|
print(str(results_path.resolve()).center(80).rstrip())
|
|
|
|
summary = "\n" + "--> LAPACK TESTING SUMMARY <--".center(80).rstrip() + "\n"
|
|
if not args.run:
|
|
summary += "Processing LAPACK Testing output found in:".center(80).rstrip()
|
|
summary += "\n" + str(test_dir.resolve()).center(80).rstrip() + "\n"
|
|
|
|
grand_total = Counts()
|
|
missing_files = 0
|
|
run_failures = 0
|
|
|
|
for suffix in suffixes:
|
|
if len(suffixes) > 1 or suffix:
|
|
summary += "\n" + section_heading(section_title(suffix)) + "\n"
|
|
if not short_summary:
|
|
print(" ")
|
|
print(section_heading(section_title(suffix)))
|
|
summary += SUMMARY_HEADER + "\n"
|
|
summary += SUMMARY_RULE + "\n"
|
|
section_total = Counts()
|
|
|
|
for letter, precision_name in PRECISIONS:
|
|
precision_cases = [case for case in cases if case.precision == letter]
|
|
if not precision_cases:
|
|
continue
|
|
precision_total = Counts()
|
|
|
|
for case in precision_cases:
|
|
output_name = case.suffixed_output(suffix)
|
|
if not just_errors and not short_summary:
|
|
print(
|
|
"Testing {} '{}' ({})".format(
|
|
precision_name, case.description, output_name
|
|
),
|
|
end=" ",
|
|
)
|
|
if args.run:
|
|
error_message = run_test_case(case, suffix, test_dir, args.bin)
|
|
if error_message is not None:
|
|
run_failures += 1
|
|
print(
|
|
"---- TESTING {}... FAILED({})!".format(
|
|
case.suffixed_executable(suffix), error_message
|
|
)
|
|
)
|
|
lines = read_output_file(test_dir / output_name)
|
|
if lines is None:
|
|
missing_files += 1
|
|
if not short_summary:
|
|
print(
|
|
"---- WARNING: please check that you have the LAPACK "
|
|
"output {}!".format(output_name)
|
|
)
|
|
print(
|
|
"---- WARNING: with the option -r, we can run the "
|
|
"LAPACK testing for you"
|
|
)
|
|
continue
|
|
log.record(output_name, lines)
|
|
report = parse_lines(case.parser, lines)
|
|
precision_total.add(report.counts)
|
|
|
|
if not short_summary:
|
|
if not just_errors:
|
|
# Finish the "Testing ..." progress line.
|
|
if report.counts.runs > 0:
|
|
print("- passed: {}".format(report.counts.runs))
|
|
else:
|
|
print("")
|
|
for line in report.notable_lines:
|
|
print("--> {}".format(line.strip()))
|
|
if report.counts.numerical > 0:
|
|
print(
|
|
"failing to pass the threshold: {}".format(
|
|
report.counts.numerical
|
|
)
|
|
)
|
|
if report.counts.illegal > 0:
|
|
print("Illegal Error: {}".format(report.counts.illegal))
|
|
if report.counts.info > 0:
|
|
print("Info Error: {}".format(report.counts.info))
|
|
if just_errors and report.counts.errors > 0:
|
|
print(
|
|
"ERROR IS LOCATED IN {} {} [ {} ]".format(
|
|
precision_name, case.description, output_name
|
|
)
|
|
)
|
|
if not just_errors or report.counts.errors > 0:
|
|
print("")
|
|
sys.stdout.flush()
|
|
|
|
summary += format_summary_row(precision_name, precision_total) + "\n"
|
|
section_total.add(precision_total)
|
|
|
|
if args.prec == "x":
|
|
summary += (
|
|
"\n" + format_summary_row("--> ALL PRECISIONS", section_total) + "\n"
|
|
)
|
|
grand_total.add(section_total)
|
|
|
|
log.close()
|
|
|
|
if args.number:
|
|
print(grand_total.numerical)
|
|
print(grand_total.other)
|
|
else:
|
|
print(summary)
|
|
if grand_total.runs == 0:
|
|
print(
|
|
"NO TESTS WERE ANALYZED, please use the -r option to run "
|
|
"the LAPACK TESTING"
|
|
)
|
|
if missing_files > 0 and short_summary:
|
|
print(
|
|
"lapack_testing.py: {} expected output file(s) were missing "
|
|
"(rerun without -s/-n for details)".format(missing_files),
|
|
file=sys.stderr,
|
|
)
|
|
|
|
unrecognized = find_unrecognized_outputs(test_dir)
|
|
if unrecognized:
|
|
print(
|
|
"lapack_testing.py: {} .out file(s) in {} are not known to this "
|
|
"script and were NOT analyzed (new tests must be added to the "
|
|
"test tables in this script):".format(len(unrecognized), test_dir),
|
|
file=sys.stderr,
|
|
)
|
|
for name in unrecognized:
|
|
print(" {}".format(name), file=sys.stderr)
|
|
|
|
if args.fail_if_empty and grand_total.runs == 0:
|
|
return 1
|
|
if args.fail_on_unrecognized and unrecognized:
|
|
return 1
|
|
if args.fail_on_error and (grand_total.errors > 0 or run_failures):
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def _configure_output_streams() -> None:
|
|
"""Make stdout/stderr replace unencodable characters instead of dying.
|
|
|
|
Fortran test output is not guaranteed to be encodable in the console
|
|
encoding (notably on Windows with a redirected stdout).
|
|
"""
|
|
for stream in (sys.stdout, sys.stderr):
|
|
if isinstance(stream, io.TextIOWrapper):
|
|
stream.reconfigure(errors="replace")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_configure_output_streams()
|
|
sys.exit(main())
|