-
Notifications
You must be signed in to change notification settings - Fork 516
Expand file tree
/
Copy pathlapack_testing.py
More file actions
executable file
·2424 lines (2135 loc) · 89.2 KB
/
Copy pathlapack_testing.py
File metadata and controls
executable file
·2424 lines (2135 loc) · 89.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""Summarize (and optionally run) the LAPACK, LAPACKE, BLAS and CBLAS 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 LAPACKE (``xlintst?_lapacke_*``), BLAS (``xblat[123]?``) and CBLAS
(``x?cblat[123]``) test drivers are analyzed too, from their own testing
directories, and are reported in their own summary sections. The
LAPACKE drivers are the linear equation tests rebuilt with the routine
calls routed through LAPACKE, one driver per precision and (API layer,
matrix layout) flavor; their output uses the classic LAPACK summary
format. The BLAS and CBLAS 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. With
``--merge-apis`` a library whose two API variants report the same errors
is summarized in one combined section instead.
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.
./lapack_testing.py -s --junit-xml results.xml
Print only the summary table and also write a JUnit XML report
of the analyzed output files, e.g. for GitLab CI test reports.
./lapack_testing.py -s --markdown summary.md
Print only the summary table and also write a GitHub-flavored
Markdown report of the test results, e.g. for GitHub Actions
step summaries ($GITHUB_STEP_SUMMARY).
"""
from __future__ import annotations
import argparse
import html
import io
import math
import os
import re
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
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"),
)
# Summary table label of each precision letter, e.g. "s" -> "REAL".
PRECISION_NAMES: "Dict[str, str]" = dict(PRECISIONS)
# 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 and CBLAS 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"),
)
# LAPACKE linear equation test flavors: (layer, layout, description).
# The double precision LIN tests are rebuilt with allowlisted routine
# calls routed through LAPACKE, once per (API layer, matrix layout)
# combination; only the work/column-major flavor runs the error-exit
# tests, so the other flavors read a generated input with those disabled.
LAPACKE_FLAVORS: "Tuple[Tuple[str, str, str], ...]" = (
("work", "cm", "column-major work-level API"),
("work", "rm", "row-major work-level API"),
("high", "cm", "column-major high-level API"),
("high", "rm", "row-major high-level API"),
)
# Libraries, in reporting order. Each has its own testing directory and
# its own section in the summary table.
LIBRARY_LAPACK = "LAPACK"
LIBRARY_LAPACKE = "LAPACKE"
LIBRARY_BLAS = "BLAS"
LIBRARY_CBLAS = "CBLAS"
LIBRARIES: "Tuple[str, ...]" = (
LIBRARY_LAPACK,
LIBRARY_LAPACKE,
LIBRARY_BLAS,
LIBRARY_CBLAS,
)
# 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 + ("lapacke", "blas", "cblas")
# Which library each family belongs to.
FAMILY_LIBRARY: "Dict[str, str]" = dict(
[(family, LIBRARY_LAPACK) for family in LAPACK_FAMILIES]
+ [
("lapacke", LIBRARY_LAPACKE),
("blas", LIBRARY_BLAS),
("cblas", LIBRARY_CBLAS),
]
)
# 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+)")
# Footer printed by every test driver, e.g.
# " Total time used = 48.32 seconds"
# This is the time the driver measured itself, so it is available even
# when this script only analyzes output files it did not run. The
# format is F12.2, which prints asterisks on overflow; such a line
# simply does not match and the case is then reported without a time.
RE_TOTAL_TIME = re.compile(r"Total time used\s*=\s*(\d+\.?\d*)\s*seconds")
# 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/CBLAS drivers, e.g.
# " SGEMV COMPUTATIONAL TESTS: 3456 RUN, 0 FAILED"
# " cblas_sgemv ROW-MAJOR COMPUTATIONAL TESTS: 3456 RUN, ..."
# The routine name field width differs per driver, so never match on
# column positions.
RE_BLAS_COUNTS = re.compile(
r"^\s*\S+\s+(?:(?:COLUMN-MAJOR|ROW-MAJOR)\s+)?"
r"(COMPUTATIONAL|ERROR-EXIT) TESTS:\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 (?:(?:COLUMN-MAJOR|ROW-MAJOR)\s+)?"
r"(?:COMPUTATIONAL TESTS|TESTS OF ERROR-EXITS)\b"
)
RE_BLAS_SUSPECT = re.compile(
r"\bCOMPLETED THE (?:(?:COLUMN-MAJOR|ROW-MAJOR)\s+)?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)
# Run time in seconds as reported by the driver in its footer, or
# None for a run that never reached that footer and for output of a
# build whose drivers do not print one.
elapsed: "Optional[float]" = None
@dataclass
class SectionResult:
"""Accumulated counts of one library/API section of the summary."""
# Per-precision rows of the summary table, in reporting order.
precisions: "List[Tuple[str, Counts]]" = field(default_factory=list)
total: Counts = field(default_factory=Counts)
# Counts per output file, keyed by the API-independent output name.
# Used to compare one API variant against another; a file that was
# missing has no entry, so a partial run never compares equal.
case_counts: "Dict[str, Counts]" = field(default_factory=dict)
def error_map(self) -> "Dict[str, Tuple[int, int, int]]":
"""Return the per-file error counts, ignoring the run counts.
Returns:
The (numerical, illegal, info) triple of every analyzed
output file, keyed by its API-independent name.
"""
return {
name: (counts.numerical, counts.illegal, counts.info)
for name, counts in self.case_counts.items()
}
@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
# The tracked source-tree file a generated input is derived from, used
# for the JUnit 'file' attribute; None when input_name itself is a
# source-tree file.
source_input: "Optional[str]" = None
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
@dataclass
class CaseOutcome:
"""Analysis outcome of one test case in one API variant.
Collected in analysis order for the JUnit XML report: the parsing
result of the output file (or None when the file was missing), the
error message of a driver run that failed under ``--run`` or of an
output file that could not be read, and the wall-clock duration of
the driver run when ``--run`` was given.
"""
case: TestCase
suffix: str
run_error: "Optional[str]" = None
report: "Optional[FileReport]" = None
duration: "Optional[float]" = None
# When the run behind this outcome took place, in seconds since the
# epoch: the wall-clock start of the driver under ``--run``,
# otherwise the modification time of the output file, which is when
# the driver that wrote it finished. None when there is no output
# file to go by.
started: "Optional[float]" = None
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 (a subset of
``ALL_FAMILIES``).
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 "lapacke" in families:
# All flavors of a driver read <x>test.in except that the
# flavors that cannot run the error-exit tests read the
# generated <x>test_noerr.in, which only exists in the build
# tree.
for layer, layout, flavor in LAPACKE_FLAVORS:
error_exits = layer == "work" and layout == "cm"
cases.append(
TestCase(
precision=letter,
family="lapacke",
description="Linear Equation routines via the "
+ flavor,
input_name="{}test.in".format(letter)
if error_exits
else "{}test_noerr.in".format(letter),
output_name="{}test_{}_{}.out".format(letter, layer, layout),
source_input=None
if error_exits
else "{}test.in".format(letter),
executable="xlintst{}_lapacke_{}_{}".format(
letter, layer, layout
),
parser=PARSER_STANDARD,
library=LIBRARY_LAPACKE,
)
)
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,
)
)
if "cblas" in families:
for level, description in BLAS_LEVELS:
# The CBLAS inputs carry no output file name, so the same
# input serves both APIs and the harness does the
# redirection for every level.
cases.append(
TestCase(
precision=letter,
family="cblas",
description="C interface to " + description,
input_name=(
None if level == 1 else "{}in{}".format(letter, level)
),
output_name="{}test{}.out".format(letter, level),
executable="x{}cblat{}".format(letter, level),
parser=PARSER_BLAS1 if level == 1 else PARSER_BLAS23,
library=LIBRARY_CBLAS,
)
)
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 or CBLAS 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_elapsed(lines: "Sequence[str]") -> "Optional[float]":
"""Return the run time the driver reported in its footer.
Args:
lines: The lines of the output file.
Returns:
The run time in seconds, or None when the output carries no
readable ``Total time used`` line. The drivers print the line
once, in their footer; should an output carry several, the last
one wins.
"""
elapsed: "Optional[float]" = None
for line in lines:
match = RE_TOTAL_TIME.search(line)
if match:
elapsed = float(match.group(1))
return elapsed
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, the notable (error) lines and the reported run time
of the file.
"""
if parser == PARSER_BALANCE:
report = parse_balance(lines)
elif parser == PARSER_DMD:
report = parse_dmd(lines)
elif parser in (PARSER_BLAS1, PARSER_BLAS23):
report = parse_blas(lines, level_one=parser == PARSER_BLAS1)
else:
report = parse_standard(lines)
# The footer is formatted the same way by every driver that prints
# one at all, so it is read here rather than in each parser.
report.elapsed = parse_elapsed(lines)
return report
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",
Path("CBLAS") / "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_LAPACKE: "TESTING",
LIBRARY_BLAS: "BLAS/TESTING",
LIBRARY_CBLAS: "CBLAS/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: