The BLAS tests run under both build systems but nothing reported on
them: the script knew only the LAPACK drivers and scanned only the
LAPACK testing directory.
Add the BLAS suite as its own library:
- A test case per precision and BLAS level, with the Level 1 drivers
marked as reading no input, and the Level 2 and 3 drivers marked as
opening their own output file.
- parse_blas, which takes run and failure counts from the driver's
report lines. Computational failures are numerical errors and
error-exit failures are other errors.
- --blas-dir, defaulting to BLAS/TESTING. That relative path is
correct from a CMake build root and from the source tree alike, so
no existing invocation needs changing. A missing directory is
skipped silently, since builds against an optimized BLAS have none.
- --run support, which needs the input-less Level 1 drivers handled
and the output redirection suppressed for Level 2 and 3. A _64 run
of Level 2 or 3 requires the generated _64 input; without it the
driver would write over the default-API output, so it is skipped
with a message instead.
The drivers exit 0 even when they abandon a run, and print END OF TESTS
even when routines failed, so breakage is read from the text: an
abandoned or misconfigured run, and a Level 2 or 3 file that never
reached its footer, each count as one other error. Detail lines behind
an already counted verdict are shown but not counted, which otherwise
inflates a failing file several fold.
Output from a build whose drivers do not report counts is still
summarized, by counting one test per verdict. That is coarse, but never
silently zero.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1368 lines
49 KiB
Python
Executable File
1368 lines
49 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Summarize (and optionally run) the LAPACK and BLAS test suites.
|
|
|
|
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.
|
|
|
|
The BLAS (``xblat[123]?``) test drivers are analyzed too, from their own
|
|
testing directory, and are reported in their own summary section. Those
|
|
drivers report their test counts in lines of the form::
|
|
|
|
SGEMV COMPUTATIONAL TESTS: 3456 RUN, 0 FAILED
|
|
SGEMV ERROR-EXIT TESTS: 6 RUN, 0 FAILED
|
|
|
|
Computational failures are counted as numerical errors and error-exit
|
|
failures as other errors. Output produced by a build whose drivers do
|
|
not report counts is still summarized, by counting one test per verdict.
|
|
|
|
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.
|
|
|
|
./lapack_testing.py -t blas
|
|
Summarize only the BLAS test output.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import io
|
|
import math
|
|
import os
|
|
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"),
|
|
)
|
|
|
|
# BLAS test sets, one per BLAS level: (level, description). The Level 1
|
|
# drivers read no input file and write to standard output.
|
|
BLAS_LEVELS: "Tuple[Tuple[int, str], ...]" = (
|
|
(1, "Level 1 BLAS routines"),
|
|
(2, "Level 2 BLAS routines"),
|
|
(3, "Level 3 BLAS routines"),
|
|
)
|
|
|
|
# Libraries, in reporting order. Each has its own testing directory and
|
|
# its own section in the summary table.
|
|
LIBRARY_LAPACK = "LAPACK"
|
|
LIBRARY_BLAS = "BLAS"
|
|
LIBRARIES: "Tuple[str, ...]" = (LIBRARY_LAPACK, LIBRARY_BLAS)
|
|
|
|
# LAPACK test families, in reporting order per precision.
|
|
LAPACK_FAMILIES: "Tuple[str, ...]" = ("eig",) + tuple(s[0] for s in LIN_SETS) + ("dmd",)
|
|
|
|
# All test families, in reporting order per precision.
|
|
ALL_FAMILIES: "Tuple[str, ...]" = LAPACK_FAMILIES + ("blas",)
|
|
|
|
# Which library each family belongs to.
|
|
FAMILY_LIBRARY: "Dict[str, str]" = dict(
|
|
[(family, LIBRARY_LAPACK) for family in LAPACK_FAMILIES] + [("blas", LIBRARY_BLAS)]
|
|
)
|
|
|
|
# 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)
|
|
|
|
# Test counts reported by the BLAS drivers, e.g.
|
|
# " SGEMV COMPUTATIONAL TESTS: 3456 RUN, 0 FAILED"
|
|
# The routine name field width differs per driver, so never match on
|
|
# column positions.
|
|
RE_BLAS_COUNTS = re.compile(
|
|
r"^\s*\S+\s+(COMPUTATIONAL|ERROR-EXIT) TESTS:" r"\s*(\d+) RUN,\s*(\d+) FAILED\s*$"
|
|
)
|
|
|
|
# Per-routine verdicts. These are only counted when the driver did not
|
|
# report counts (output from a build without the counting instrumentation).
|
|
RE_BLAS_PASSED = re.compile(
|
|
r"^\s*\S+\s+PASSED THE (?:COMPUTATIONAL TESTS|TESTS OF ERROR-EXITS)\b"
|
|
)
|
|
RE_BLAS_SUSPECT = re.compile(r"\bCOMPLETED THE COMPUTATIONAL TESTS\b")
|
|
RE_BLAS_FAILED_COMPUTATIONAL = re.compile(r"\bFAILED ON CALL NUMBER:")
|
|
RE_BLAS_FAILED_ERROR_EXIT = re.compile(r"\bFAILED THE TESTS OF ERROR-EXITS\b")
|
|
|
|
# Driver-level breakage that the per-routine counts cannot express: the
|
|
# run was abandoned, misconfigured, or never reached its footer.
|
|
RE_BLAS_ABANDONED = re.compile(r"\*{5,7} (?:FATAL ERROR - )?TESTS ABANDONED \*{5,7}")
|
|
RE_BLAS_NOT_RECOGNIZED = re.compile(r"^\s*SUBPROGRAM NAME .* NOT RECOGNIZED")
|
|
RE_BLAS_DOT_PRODUCTS = re.compile(r"^\s*ERROR IN [SDCZ]M[VM]T?CH\b")
|
|
RE_BLAS_INTERNAL = re.compile(r"Shouldn't be here in CHECK")
|
|
RE_BLAS_INPUT_ERROR = re.compile(
|
|
r"^\s*(?:NUMBER OF VALUES OF |VALUE OF [NK] IS LESS THAN"
|
|
r"|ABSOLUTE VALUE OF INCX OR INCY )"
|
|
)
|
|
|
|
# Detail lines that sit behind a verdict which is already counted. They
|
|
# are worth showing but must not be counted: the ``cblat2_64.out`` fixture
|
|
# has 91 of them behind just 17 failing routines.
|
|
RE_BLAS_DETAIL = re.compile(
|
|
r"XERBLA WAS CALLED WITH"
|
|
r"|ILLEGAL VALUE OF PARAMETER NUMBER"
|
|
r"|FATAL ERROR - COMPUTED RESULT IS LESS THAN HALF ACCURATE"
|
|
r"|FATAL ERROR - PARAMETER NUMBER"
|
|
r"|FATAL ERROR - ERROR-EXIT TAKEN ON VALID CALL"
|
|
r"|BUT WITH MAXIMUM TEST RATIO"
|
|
r"|WARNING: Skipping xerbla tests"
|
|
)
|
|
RE_BLAS_NOT_TESTED = re.compile(r"^\s*\S+\s+WAS NOT TESTED\s*$")
|
|
|
|
# Level 2/3 footer. Note this is printed even when routines failed, so
|
|
# it means "not truncated", not "passed"; its absence means the driver
|
|
# died part way through.
|
|
RE_BLAS_END_OF_TESTS = re.compile(r"^\s*END OF TESTS\s*$")
|
|
|
|
# Level 1 drivers have no counts of their own in an uninstrumented build
|
|
# and no footer at all; one "Test of subprogram number" block is one
|
|
# subprogram, followed by either a PASS line or FAIL detail.
|
|
RE_BLAS_L1_CASE = re.compile(r"^\s*Test of subprogram number\s*\d+")
|
|
RE_BLAS_L1_PASS = re.compile(r"^\s*-{5} PASS -{5}\s*$")
|
|
RE_BLAS_L1_FAIL = re.compile(r"^\s*FAIL\s*$")
|
|
|
|
# Parser kinds, used by TestCase.parser.
|
|
PARSER_STANDARD = "standard"
|
|
PARSER_BALANCE = "balance"
|
|
PARSER_DMD = "dmd"
|
|
PARSER_BLAS1 = "blas1"
|
|
PARSER_BLAS23 = "blas23"
|
|
|
|
|
|
@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 test driver invocation and its expected output file."""
|
|
|
|
precision: str
|
|
family: str
|
|
description: str
|
|
input_name: "Optional[str]"
|
|
output_name: str
|
|
executable: str
|
|
parser: str
|
|
library: str = LIBRARY_LAPACK
|
|
# True when the API suffix also applies to the input file name. The
|
|
# BLAS Level 2/3 inputs name the output file on their first line, so
|
|
# the _64 run needs the generated _64 input; every other driver takes
|
|
# the same input for both APIs.
|
|
input_suffixed: bool = False
|
|
# False when the driver opens its own output file, so the harness must
|
|
# not also redirect standard output onto it.
|
|
redirect_stdout: bool = True
|
|
|
|
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_input(self, suffix: str) -> "Optional[str]":
|
|
"""Return the input file name for an API suffix.
|
|
|
|
Args:
|
|
suffix: The API suffix, either ``""`` or ``"_64"``.
|
|
|
|
Returns:
|
|
The input file name, or None for the drivers that read no
|
|
input at all.
|
|
"""
|
|
if self.input_name is None or not self.input_suffixed or not suffix:
|
|
return self.input_name
|
|
stem, _, extension = self.input_name.rpartition(".")
|
|
return "{}{}.{}".format(stem, suffix, extension)
|
|
|
|
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,
|
|
)
|
|
)
|
|
if "blas" in families:
|
|
for level, description in BLAS_LEVELS:
|
|
# Level 1 reads no input; Level 2/3 read e.g. sblat2.in,
|
|
# whose first line names the output file, so the _64 run
|
|
# needs the generated sblat2_64.in.
|
|
cases.append(
|
|
TestCase(
|
|
precision=letter,
|
|
family="blas",
|
|
description=description,
|
|
input_name=(
|
|
None if level == 1 else "{}blat{}.in".format(letter, level)
|
|
),
|
|
output_name="{}blat{}.out".format(letter, level),
|
|
executable="xblat{}{}".format(level, letter),
|
|
parser=PARSER_BLAS1 if level == 1 else PARSER_BLAS23,
|
|
library=LIBRARY_BLAS,
|
|
input_suffixed=level != 1,
|
|
redirect_stdout=level == 1,
|
|
)
|
|
)
|
|
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_blas(lines: "Sequence[str]", level_one: bool) -> FileReport:
|
|
"""Parse a BLAS test output file.
|
|
|
|
Test counts come from the ``... TESTS: n RUN, m FAILED`` lines the
|
|
drivers report per routine: computational failures are numerical
|
|
errors, error-exit failures are other errors. Output from a build
|
|
whose drivers do not report counts is still summarized, by falling
|
|
back to one test per verdict.
|
|
|
|
The drivers exit with status 0 even when they abandon the run, and
|
|
print ``END OF TESTS`` even when routines failed, so breakage is
|
|
detected from the text: an abandoned or misconfigured run, and a
|
|
Level 2/3 file that never reached its footer, each count as one other
|
|
error.
|
|
|
|
Args:
|
|
lines: The lines of the output file.
|
|
level_one: True for the Level 1 drivers, which have no footer.
|
|
|
|
Returns:
|
|
The counts and the notable (error) lines of the file.
|
|
"""
|
|
report = FileReport()
|
|
reported_counts = False
|
|
saw_footer = False
|
|
abandoned = False
|
|
# Verdict tallies, used only if the driver reported no counts.
|
|
verdicts = Counts()
|
|
cases = 0
|
|
case_failed = False
|
|
|
|
for line in lines:
|
|
match = RE_BLAS_COUNTS.match(line)
|
|
if match:
|
|
reported_counts = True
|
|
report.counts.runs += int(match.group(2))
|
|
failures = int(match.group(3))
|
|
if match.group(1) == "COMPUTATIONAL":
|
|
report.counts.numerical += failures
|
|
else:
|
|
report.counts.illegal += failures
|
|
continue
|
|
|
|
if RE_BLAS_END_OF_TESTS.match(line):
|
|
saw_footer = True
|
|
continue
|
|
|
|
# Driver-level breakage, which no per-routine count can express.
|
|
if (
|
|
RE_BLAS_ABANDONED.search(line)
|
|
or RE_BLAS_NOT_RECOGNIZED.match(line)
|
|
or RE_BLAS_DOT_PRODUCTS.match(line)
|
|
or RE_BLAS_INTERNAL.search(line)
|
|
or RE_BLAS_INPUT_ERROR.match(line)
|
|
):
|
|
abandoned = True
|
|
report.counts.info += 1
|
|
report.notable_lines.append(line)
|
|
continue
|
|
|
|
if RE_BLAS_FAILED_ERROR_EXIT.search(line):
|
|
verdicts.runs += 1
|
|
verdicts.illegal += 1
|
|
report.notable_lines.append(line)
|
|
continue
|
|
if RE_BLAS_FAILED_COMPUTATIONAL.search(line):
|
|
verdicts.runs += 1
|
|
verdicts.numerical += 1
|
|
report.notable_lines.append(line)
|
|
continue
|
|
if RE_BLAS_SUSPECT.search(line):
|
|
verdicts.runs += 1
|
|
verdicts.numerical += 1
|
|
report.notable_lines.append(line)
|
|
continue
|
|
if RE_BLAS_PASSED.match(line):
|
|
verdicts.runs += 1
|
|
continue
|
|
|
|
if RE_BLAS_DETAIL.search(line) or RE_BLAS_NOT_TESTED.match(line):
|
|
report.notable_lines.append(line)
|
|
continue
|
|
|
|
if level_one:
|
|
if RE_BLAS_L1_CASE.match(line):
|
|
cases += 1
|
|
case_failed = False
|
|
continue
|
|
# A NRM2 stress failure prints FAIL without clearing PASS, so
|
|
# a case can report both; treat any FAIL as a failure.
|
|
if RE_BLAS_L1_FAIL.match(line) and not case_failed:
|
|
case_failed = True
|
|
verdicts.numerical += 1
|
|
report.notable_lines.append(line)
|
|
|
|
if not reported_counts:
|
|
if level_one:
|
|
verdicts.runs += cases
|
|
report.counts.add(verdicts)
|
|
|
|
# A run that reported why it stopped has already been counted.
|
|
if not level_one and not saw_footer and not abandoned:
|
|
report.counts.info += 1
|
|
report.notable_lines.append(
|
|
"output ends without 'END OF TESTS': the driver did not finish\n"
|
|
)
|
|
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``,
|
|
``PARSER_DMD``, ``PARSER_BLAS1`` and ``PARSER_BLAS23``.
|
|
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)
|
|
if parser in (PARSER_BLAS1, PARSER_BLAS23):
|
|
return parse_blas(lines, level_one=parser == PARSER_BLAS1)
|
|
return parse_standard(lines)
|
|
|
|
|
|
def find_unrecognized_outputs(directories: "Dict[str, Path]") -> "List[str]":
|
|
"""Find ``.out`` files in the test directories this script cannot analyze.
|
|
|
|
A file is unrecognized if its name matches no known test case of the
|
|
library that owns its directory, 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:
|
|
directories: The existing testing directory of each library.
|
|
|
|
Returns:
|
|
The unrecognized file names, prefixed by their directory when
|
|
more than one directory was scanned, sorted alphabetically.
|
|
"""
|
|
all_cases = build_test_cases("sdcz", ALL_FAMILIES)
|
|
unrecognized: "List[str]" = []
|
|
for library, directory in directories.items():
|
|
known = {
|
|
case.suffixed_output(suffix)
|
|
for case in all_cases
|
|
if case.library == library
|
|
for suffix in KNOWN_SUFFIXES
|
|
}
|
|
known.add(RESULTS_FILENAME)
|
|
for path in directory.glob("*.out"):
|
|
if path.name in known:
|
|
continue
|
|
unrecognized.append(
|
|
path.name if len(directories) == 1 else str(directory / path.name)
|
|
)
|
|
return sorted(unrecognized)
|
|
|
|
|
|
def discover_suffixes(cases: "Sequence[TestCase]", directory: Path) -> "List[str]":
|
|
"""Detect which API variants have output files in a testing directory.
|
|
|
|
Args:
|
|
cases: The selected test cases of one library.
|
|
directory: That library's testing directory.
|
|
|
|
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((directory / 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",
|
|
Path("BLAS") / "TESTING",
|
|
]
|
|
for directory in directories:
|
|
for filename in (name, name + ".exe"):
|
|
candidate = directory / filename
|
|
if candidate.is_file():
|
|
return candidate.resolve()
|
|
return None
|
|
|
|
|
|
SOURCE_INPUT_DIRS: "Dict[str, str]" = {
|
|
LIBRARY_LAPACK: "TESTING",
|
|
LIBRARY_BLAS: "BLAS/TESTING",
|
|
}
|
|
|
|
|
|
def run_test_case(
|
|
case: TestCase, suffix: str, test_dir: Path, bin_dir: "Optional[str]"
|
|
) -> "Optional[str]":
|
|
"""Run one test driver, capturing its output in 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_name = case.suffixed_input(suffix)
|
|
input_path: "Optional[Path]" = None
|
|
if input_name is not None:
|
|
input_path = test_dir / input_name
|
|
if not input_path.is_file():
|
|
# CMake build trees hold only the .out files; the .in files
|
|
# live in the source tree next to this script. The _64 input
|
|
# of a BLAS Level 2/3 driver is generated into the build tree
|
|
# and has no source-tree counterpart.
|
|
source_input = (
|
|
Path(__file__).resolve().parent
|
|
/ SOURCE_INPUT_DIRS[case.library]
|
|
/ input_name
|
|
)
|
|
if source_input.is_file():
|
|
input_path = source_input
|
|
elif case.input_suffixed and suffix:
|
|
# Falling back to the default-API input would make the
|
|
# driver overwrite the default-API output file.
|
|
return (
|
|
"input file {} not found (needed to keep the {} output "
|
|
"separate)".format(input_path, suffix)
|
|
)
|
|
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. Drivers that
|
|
# open their own output file (the BLAS Level 2/3 testers, which take
|
|
# its name from the first line of their input) write it directly.
|
|
temporary_path = output_path.with_name(output_path.name + ".tmp")
|
|
try:
|
|
with (
|
|
open(str(input_path), "rb")
|
|
if input_path is not None
|
|
else open(os.devnull, "rb")
|
|
) as stdin, open(
|
|
str(temporary_path) if case.redirect_stdout else os.devnull, "wb"
|
|
) as stdout:
|
|
process = subprocess.run(
|
|
[str(executable)],
|
|
stdin=stdin,
|
|
stdout=stdout,
|
|
stderr=subprocess.STDOUT,
|
|
cwd=str(test_dir),
|
|
)
|
|
except OSError as error:
|
|
if case.redirect_stdout:
|
|
try:
|
|
temporary_path.unlink()
|
|
except OSError:
|
|
pass
|
|
return "{} could not be run: {}".format(executable_name, error)
|
|
if case.redirect_stdout:
|
|
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(library: str, suffix: str) -> str:
|
|
"""Return the human-readable name of a library/API section.
|
|
|
|
Args:
|
|
library: The library name, e.g. ``"BLAS"``.
|
|
suffix: The API suffix, either ``""`` or ``"_64"``.
|
|
|
|
Returns:
|
|
The section name used in headings and messages, e.g.
|
|
``"BLAS: Extended API (_64)"``.
|
|
"""
|
|
if suffix:
|
|
return "{}: Extended API ({})".format(library, suffix)
|
|
return "{}: Default API".format(library)
|
|
|
|
|
|
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 and "
|
|
"BLAS test suites and print a summary of the test results.",
|
|
epilog="By default all precisions and all test families are "
|
|
"analyzed, each library is reported in its own section, 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(
|
|
"--blas-dir",
|
|
default=str(Path("BLAS") / "TESTING"),
|
|
help="directory containing the BLAS testing output (.out) files; "
|
|
"skipped without warning if it does not exist (default: %(default)s)",
|
|
)
|
|
parser.add_argument(
|
|
"-b",
|
|
"--bin",
|
|
default=None,
|
|
help="directory containing the test drivers for --run; by default "
|
|
"bin, bin/Release, bin/Debug, TESTING/LIN, TESTING/EIG and "
|
|
"BLAS/TESTING 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) + ["lapack", "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, blas=BLAS, "
|
|
"lapack=all LAPACK families, 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 a 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 a testing directory",
|
|
)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def main(argv: "Optional[Sequence[str]]" = None) -> int:
|
|
"""Run the LAPACK and BLAS 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
|
|
|
|
# The BLAS tests are absent from builds that use an optimized BLAS, so
|
|
# a missing directory is normal and is skipped silently. An
|
|
# explicitly selected library that has no directory is a usage error,
|
|
# though.
|
|
directories: "Dict[str, Path]" = {LIBRARY_LAPACK: test_dir}
|
|
for library, option in ((LIBRARY_BLAS, args.blas_dir),):
|
|
directory = Path(option)
|
|
if directory.is_dir():
|
|
directories[library] = directory
|
|
elif FAMILY_LIBRARY.get(args.test) == library:
|
|
print(
|
|
"lapack_testing.py: {} testing directory {} not found".format(
|
|
library, directory
|
|
),
|
|
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"
|
|
if args.test == "all":
|
|
families = list(ALL_FAMILIES)
|
|
elif args.test == "lapack":
|
|
families = list(LAPACK_FAMILIES)
|
|
else:
|
|
families = [args.test]
|
|
# Drop the families of libraries that were not built.
|
|
families = [family for family in families if FAMILY_LIBRARY[family] in directories]
|
|
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
|
|
|
|
forced_suffixes: "Optional[List[str]]" = None
|
|
if args.suffix is not None:
|
|
forced_suffixes = []
|
|
for choice in args.suffix:
|
|
suffix = "" if choice == "none" else "_64"
|
|
if suffix not in forced_suffixes:
|
|
forced_suffixes.append(suffix)
|
|
elif args.run:
|
|
forced_suffixes = [""]
|
|
|
|
# One (library, API) pair per summary section, in reporting order.
|
|
sections: "List[Tuple[str, str]]" = []
|
|
for library in LIBRARIES:
|
|
library_cases = [case for case in cases if case.library == library]
|
|
if not library_cases:
|
|
continue
|
|
suffixes = forced_suffixes
|
|
if suffixes is None:
|
|
suffixes = discover_suffixes(library_cases, directories[library])
|
|
for suffix in suffixes:
|
|
sections.append((library, suffix))
|
|
|
|
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 library, suffix in sections:
|
|
directory = directories[library]
|
|
title = section_title(library, suffix)
|
|
if len(sections) > 1 or suffix:
|
|
summary += "\n" + section_heading(title) + "\n"
|
|
if not short_summary:
|
|
print(" ")
|
|
print(section_heading(title))
|
|
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 and case.library == library
|
|
]
|
|
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, directory, 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(directory / output_name)
|
|
if lines is None:
|
|
missing_files += 1
|
|
if not short_summary:
|
|
print(
|
|
"---- WARNING: please check that you have the {} "
|
|
"output {}!".format(library, output_name)
|
|
)
|
|
print(
|
|
"---- WARNING: with the option -r, we can run the "
|
|
"testing for you"
|
|
)
|
|
continue
|
|
log.record(
|
|
(
|
|
output_name
|
|
if library == LIBRARY_LAPACK
|
|
else str(directory / 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(directories)
|
|
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),
|
|
", ".join(str(directory) for directory in directories.values()),
|
|
),
|
|
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())
|