blob: 85ec4c5ad6f823e5a9a6aa6fe5f14b5a9fc9cb38 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.rng.examples.stress;
import org.apache.commons.rng.simple.RandomSource;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Formatter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Specification for the "results" command.
*
* <p>This command creates a summary of the results from known test applications.</p>
*
* <ul>
* <li>Dieharder
* <li>Test U01 (BigCrush, Crush, SmallCrush)
* </ul>
*/
@Command(name = "results",
description = {"Collate results from stress test applications."})
class ResultsCommand implements Callable<Void> {
/** The pattern to identify the RandomSource in the stress test result header. */
private static final Pattern RANDOM_SOURCE_PATTERN = Pattern.compile("^# RandomSource: (.*)");
/** The pattern to identify the RNG in the stress test result header. */
private static final Pattern RNG_PATTERN = Pattern.compile("^# RNG: (.*)");
/** The pattern to identify the test exit code in the stress test result footer. */
private static final Pattern TEST_EXIT_PATTERN = Pattern.compile("^# Exit value: (\\d+)");
/** The pattern to identify the Dieharder test format. */
private static final Pattern DIEHARDER_PATTERN = Pattern.compile("^# *dieharder version");
/** The pattern to identify a Dieharder failed test result. */
private static final Pattern DIEHARDER_FAILED_PATTERN = Pattern.compile("FAILED *$");
/** The pattern to identify the Test U01 test format. */
private static final Pattern TESTU01_PATTERN = Pattern.compile("^ *Version: TestU01");
/** The pattern to identify the Test U01 summary header. */
private static final Pattern TESTU01_SUMMARY_PATTERN = Pattern.compile("^========= Summary results of (\\S*) ");
/** The pattern to identify the Test U01 failed test result. */
private static final Pattern TESTU01_TEST_RESULT_PATTERN = Pattern.compile("^ ?(\\d+ .*) ");
/** The pattern to identify the Test U01 test starting entry. */
private static final Pattern TESTU01_STARTING_PATTERN = Pattern.compile("^ *Starting (\\S*)");
/** The pattern to identify the PractRand test format. */
private static final Pattern PRACTRAND_PATTERN = Pattern.compile("PractRand version");
/** The pattern to identify the PractRand output byte size. */
private static final Pattern PRACTRAND_OUTPUT_SIZE_PATTERN = Pattern.compile("\\(2\\^(\\d+) bytes\\)");
/** The pattern to identify a PractRand failed test result. */
private static final Pattern PRACTRAND_FAILED_PATTERN = Pattern.compile("FAIL *!* *$");
/** The name of the Dieharder sums test. */
private static final String DIEHARDER_SUMS = "diehard_sums";
/** The string identifying a bit-reversed generator. */
private static final String BIT_REVERSED = "Bit-reversed";
/** Character '\'. */
private static final char FORWARD_SLASH = '\\';
/** Character '/'. */
private static final char BACK_SLASH = '\\';
/** Character '|'. */
private static final char PIPE = '|';
/** The column name for the RNG. */
private static final String COLUMN_RNG = "RNG";
/** Constant for conversion of bytes to binary units, prefixed with a space. */
private static final String[] BINARY_UNITS = {" B", " KiB", " MiB", " GiB", " TiB", " PiB", " EiB"};
/** The standard options. */
@Mixin
private StandardOptions reusableOptions;
/** The results files. */
@Parameters(arity = "1..*",
description = "The results files.",
paramLabel = "<file>")
private List<File> resultsFiles = new ArrayList<>();
/** The file output prefix. */
@Option(names = {"-o", "--out"},
description = "The output file (default: stdout).")
private File fileOutput;
/** The output format. */
@Option(names = {"-f", "--format"},
description = {"Output format (default: ${DEFAULT-VALUE}).",
"Valid values: ${COMPLETION-CANDIDATES}."})
private ResultsCommand.OutputFormat outputFormat = OutputFormat.TXT;
/** The flag to show failed tests. */
@Option(names = {"--failed"},
description = {"Output failed tests (support varies by format).",
"CSV: List individual test failures.",
"APT: Count of systematic test failures."})
private boolean showFailedTests;
/** The flag to include the Dieharder sums test. */
@Option(names = {"--include-sums"},
description = "Include Dieharder sums test.")
private boolean includeDiehardSums;
/** The common file path prefix used when outputting file paths. */
@Option(names = {"--path-prefix"},
description = {"Common path prefix.",
"If specified this will replace the common prefix from all " +
"files when the path is output, e.g. for the APT report."})
private String pathPrefix = "";
/** The flag to ignore partial results. */
@Option(names = {"-i", "--ignore"},
description = "Ignore partial results.")
private boolean ignorePartialResults;
/** The flag to delete partial results files. */
@Option(names = {"--delete"},
description = {"Delete partial results files.",
"This is not reversible!"})
private boolean deletePartialResults;
/**
* The output mode for the results.
*/
enum OutputFormat {
/** Comma-Separated Values (CSV) output. */
CSV,
/** Almost Plain Text (APT) output. */
APT,
/** Text table output. */
TXT,
/** Systematic failures output. */
FAILURES,
}
/**
* The test application file format.
*/
enum TestFormat {
/** Dieharder. */
DIEHARDER,
/** Test U01. */
TESTU01,
/** PractRand. */
PRACTRAND,
}
/**
* Encapsulate the results of a test application.
*/
private static class TestResult {
/** The result file. */
private final File resultFile;
/** The random source. */
private final RandomSource randomSource;
/** Flag indicating the generator was bit reversed. */
private final boolean bitReversed;
/** The test application format. */
private final TestFormat testFormat;
/** The names of the failed tests. */
private final List<String> failedTests = new ArrayList<>();
/** The test application name. */
private String testApplicationName;
/**
* Store the exit code.
* Initialised to {@link Integer#MIN_VALUE}. Exit values are expected to be 8-bit numbers
* with zero for success.
*/
private int exitCode = Integer.MIN_VALUE;
/**
* @param resultFile the result file
* @param randomSource the random source
* @param bitReversed the bit reversed flag
* @param testFormat the test format
*/
TestResult(File resultFile,
RandomSource randomSource,
boolean bitReversed,
TestFormat testFormat) {
this.resultFile = resultFile;
this.randomSource = randomSource;
this.bitReversed = bitReversed;
this.testFormat = testFormat;
}
/**
* Adds the failed test.
*
* @param testId the test id
*/
void addFailedTest(String testId) {
failedTests.add(testId);
}
/**
* Gets the result file.
*
* @return the result file
*/
File getResultFile() {
return resultFile;
}
/**
* Gets the random source.
*
* @return the random source
*/
RandomSource getRandomSource() {
return randomSource;
}
/**
* Checks if the generator was bit reversed.
*
* @return true if bit reversed
*/
boolean isBitReversed() {
return bitReversed;
}
/**
* Gets the test format.
*
* @return the test format
*/
TestFormat getTestFormat() {
return testFormat;
}
/**
* Gets the failed tests.
*
* @return the failed tests
*/
List<String> getFailedTests() {
return failedTests;
}
/**
* Gets the failure count.
*
* @return the failure count
*/
int getFailureCount() {
return failedTests.size();
}
/**
* Gets the a failure summary string.
*
* <p>The default implementation is the count of the number of failures.
* This will be negative if the test is not complete.</p>
*
* @return the failure summary
*/
String getFailureSummaryString() {
return (isComplete()) ? Integer.toString(failedTests.size()) : "-" + failedTests.size();
}
/**
* Sets the test application name.
*
* @param testApplicationName the new test application name
*/
void setTestApplicationName(String testApplicationName) {
this.testApplicationName = testApplicationName;
}
/**
* Gets the test application name.
*
* @return the test application name
*/
String getTestApplicationName() {
return testApplicationName == null ? String.valueOf(getTestFormat()) : testApplicationName;
}
/**
* Checks if the test result is complete.
* This is {@code true} only if the exit code was found and is zero.
*
* @return true if complete
*/
boolean isComplete() {
return exitCode == 0;
}
/**
* Sets the exit code flag.
*
* @param exitCode the new exit code
*/
void setExitCode(int exitCode) {
this.exitCode = exitCode;
}
}
/**
* Encapsulate the results of a PractRand test application. This is a specialisation that
* allows handling PractRand results which are linked to the output length used by the RNG.
*/
private static class PractRandTestResult extends TestResult {
/** The length of the RNG output used to generate failed tests. */
private int lengthExponent;
/**
* @param resultFile the result file
* @param randomSource the random source
* @param bitReversed the bit reversed flag
* @param testFormat the test format
*/
PractRandTestResult(File resultFile, RandomSource randomSource, boolean bitReversed, TestFormat testFormat) {
super(resultFile, randomSource, bitReversed, testFormat);
}
/**
* Gets the length of the RNG output used to generate failed tests.
* If this is zero then no failures occurred.
*
* @return the length exponent
*/
int getLengthExponent() {
return lengthExponent;
}
/**
* Sets the length of the RNG output used to generate failed tests.
*
* @param lengthExponent the length exponent
*/
void setLengthExponent(int lengthExponent) {
this.lengthExponent = lengthExponent;
}
/**
* {@inheritDoc}
*
* <p>The PractRand summary is the exponent of the length of byte output from the RNG where
* a failure occurred.
*/
@Override
String getFailureSummaryString() {
return lengthExponent == 0 ? "-" : Integer.toString(lengthExponent);
}
}
/**
* Reads the results files and outputs in the chosen format.
*/
@Override
public Void call() {
LogUtils.setLogLevel(reusableOptions.logLevel);
// Read the results
final List<TestResult> results = readResults();
if (deletePartialResults) {
deleteIfIncomplete(results);
return null;
}
try (OutputStream out = createOutputStream()) {
switch (outputFormat) {
case CSV:
writeCSVData(out, results);
break;
case APT:
writeAPT(out, results);
break;
case TXT:
writeTXT(out, results);
break;
case FAILURES:
writeFailures(out, results);
break;
default:
throw new ApplicationException("Unknown output format: " + outputFormat);
}
} catch (final IOException ex) {
throw new ApplicationException("IO error: " + ex.getMessage(), ex);
}
return null;
}
/**
* Read the results.
*
* @return the results
*/
private List<TestResult> readResults() {
final ArrayList<TestResult> results = new ArrayList<>();
for (final File resultFile : resultsFiles) {
readResults(results, resultFile);
}
return results;
}
/**
* Read the file and extract any test results.
*
* @param results Results.
* @param resultFile Result file.
* @throws ApplicationException If the results cannot be parsed.
*/
private void readResults(List<TestResult> results,
File resultFile) {
final List<String> contents = readFileContents(resultFile);
// Files may have multiple test results per file (i.e. appended output)
final List<List<String>> outputs = splitContents(contents);
if (outputs.isEmpty()) {
LogUtils.error("No test output in file: %s", resultFile);
} else {
for (final List<String> testOutput : outputs) {
final TestResult result = readResult(resultFile, testOutput);
if (!result.isComplete()) {
LogUtils.info("Partial results in file: %s", resultFile);
if (ignorePartialResults) {
continue;
}
}
results.add(result);
}
}
}
/**
* Read the file contents.
*
* @param resultFile Result file.
* @return the file contents
* @throws ApplicationException If the file cannot be read.
*/
private static List<String> readFileContents(File resultFile) {
final ArrayList<String> contents = new ArrayList<>();
try (BufferedReader reader = Files.newBufferedReader(resultFile.toPath())) {
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
contents.add(line);
}
} catch (final IOException ex) {
throw new ApplicationException("Failed to read file contents: " + resultFile, ex);
}
return contents;
}
/**
* Split the file contents into separate test output. This is used in the event
* that results have been appended to the same file.
*
* @param contents File contents.
* @return the test output
*/
private static List<List<String>> splitContents(List<String> contents) {
final ArrayList<List<String>> testOutputs = new ArrayList<>();
// Assume each output begins with e.g. # RandomSource: SPLIT_MIX_64
// Note each beginning.
int begin = -1;
for (int i = 0; i < contents.size(); i++) {
if (RANDOM_SOURCE_PATTERN.matcher(contents.get(i)).matches()) {
if (begin >= 0) {
testOutputs.add(contents.subList(begin, i));
}
begin = i;
}
}
if (begin >= 0) {
testOutputs.add(contents.subList(begin, contents.size()));
}
return testOutputs;
}
/**
* Read the file into a test result.
*
* @param resultFile Result file.
* @param testOutput Test output.
* @return the test result
* @throws ApplicationException If the result cannot be parsed.
*/
private TestResult readResult(File resultFile,
List<String> testOutput) {
// Use an iterator for a single pass over the test output
final ListIterator<String> iter = testOutput.listIterator();
// Identify the RandomSource and bit reversed flag from the header:
final RandomSource randomSource = getRandomSource(resultFile, iter);
final boolean bitReversed = getBitReversed(resultFile, iter);
// Identify the test application format. This may return null.
final TestFormat testFormat = getTestFormat(resultFile, iter);
// Read the application results
final TestResult testResult = createTestResult(resultFile, randomSource, bitReversed, testFormat);
if (testFormat == TestFormat.DIEHARDER) {
readDieharder(iter, testResult);
} else if (testFormat == TestFormat.TESTU01) {
readTestU01(resultFile, iter, testResult);
} else {
readPractRand(iter, (PractRandTestResult) testResult);
}
return testResult;
}
/**
* Creates the test result.
*
* @param resultFile Result file.
* @param randomSource Random source.
* @param bitReversed True if the random source was bit reversed.
* @param testFormat Test format.
* @return the test result
*/
private static TestResult createTestResult(File resultFile, RandomSource randomSource,
boolean bitReversed, TestFormat testFormat) {
return testFormat == TestFormat.PRACTRAND ?
new PractRandTestResult(resultFile, randomSource, bitReversed, testFormat) :
new TestResult(resultFile, randomSource, bitReversed, testFormat);
}
/**
* Gets the random source from the output header.
*
* @param resultFile Result file (for the exception message).
* @param iter Iterator of the test output.
* @return the random source
* @throws ApplicationException If the RandomSource header line cannot be found.
*/
private static RandomSource getRandomSource(File resultFile, Iterator<String> iter) {
while (iter.hasNext()) {
final Matcher matcher = RANDOM_SOURCE_PATTERN.matcher(iter.next());
if (matcher.matches()) {
return RandomSource.valueOf(matcher.group(1));
}
}
throw new ApplicationException("Failed to find RandomSource header line: " + resultFile);
}
/**
* Gets the bit-reversed flag from the output header.
*
* @param resultFile Result file (for the exception message).
* @param iter Iterator of the test output.
* @return the bit-reversed flag
* @throws ApplicationException If the RNG header line cannot be found.
*/
private static boolean getBitReversed(File resultFile, Iterator<String> iter) {
while (iter.hasNext()) {
final Matcher matcher = RNG_PATTERN.matcher(iter.next());
if (matcher.matches()) {
return matcher.group(1).contains(BIT_REVERSED);
}
}
throw new ApplicationException("Failed to find RNG header line: " + resultFile);
}
/**
* Gets the test format from the output. This scans the stdout produced by a test application.
* If it is not recognised this may be a valid partial result or an unknown result. Throw
* an exception if not allowing partial results, otherwise log an error.
*
* @param resultFile Result file (for the exception message).
* @param iter Iterator of the test output.
* @return the test format (or null)
* @throws ApplicationException If the test format cannot be found.
*/
private TestFormat getTestFormat(File resultFile, Iterator<String> iter) {
while (iter.hasNext()) {
final String line = iter.next();
if (DIEHARDER_PATTERN.matcher(line).find()) {
return TestFormat.DIEHARDER;
}
if (TESTU01_PATTERN.matcher(line).find()) {
return TestFormat.TESTU01;
}
if (PRACTRAND_PATTERN.matcher(line).find()) {
return TestFormat.PRACTRAND;
}
}
if (!ignorePartialResults) {
throw new ApplicationException("Failed to identify the test application format: " + resultFile);
}
LogUtils.error("Failed to identify the test application format: %s", resultFile);
return null;
}
/**
* Read the result output from the Dieharder test application.
*
* @param iter Iterator of the test output.
* @param testResult Test result.
*/
private void readDieharder(Iterator<String> iter,
TestResult testResult) {
// Dieharder results are printed in-line using the following format:
//#=============================================================================#
// test_name |ntup| tsamples |psamples| p-value |Assessment
//#=============================================================================#
// diehard_birthdays| 0| 100| 100|0.97484422| PASSED
// ...
// diehard_oqso| 0| 2097152| 100|0.00000000| FAILED
testResult.setTestApplicationName("Dieharder");
// Identify any line containing FAILED and then get the test ID using
// the first two columns (test_name + ntup).
while (iter.hasNext()) {
final String line = iter.next();
if (DIEHARDER_FAILED_PATTERN.matcher(line).find()) {
// Optionally include the flawed Dieharder sums test
if (!includeDiehardSums && line.contains(DIEHARDER_SUMS)) {
continue;
}
final int index1 = line.indexOf('|');
final int index2 = line.indexOf('|', index1 + 1);
testResult.addFailedTest(line.substring(0, index1).trim() + ":" +
line.substring(index1 + 1, index2).trim());
} else if (findExitCode(testResult, line)) {
return;
}
}
}
/**
* Find the exit code in the line. Update the test result with the code if found.
*
* @param testResult Test result.
* @param line Line from the test result output.
* @return true, if the exit code was found
*/
private static boolean findExitCode(TestResult testResult, String line) {
final Matcher matcher = TEST_EXIT_PATTERN.matcher(line);
if (matcher.find()) {
testResult.setExitCode(Integer.parseInt(matcher.group(1)));
return true;
}
return false;
}
/**
* Read the result output from the Test U01 test application.
*
* <p>Test U01 outputs a summary of results at the end of the test output. If this cannot
* be found the method will throw an exception unless partial results are allowed.</p>
*
* @param resultFile Result file (for the exception message).
* @param iter Iterator of the test output.
* @param testResult Test result.
* @throws ApplicationException If the TestU01 summary cannot be found.
*/
private void readTestU01(File resultFile,
ListIterator<String> iter,
TestResult testResult) {
// Results are summarised at the end of the file:
//========= Summary results of BigCrush =========
//
//Version: TestU01 1.2.3
//Generator: stdin
//Number of statistics: 155
//Total CPU time: 06:14:50.43
//The following tests gave p-values outside [0.001, 0.9990]:
//(eps means a value < 1.0e-300):
//(eps1 means a value < 1.0e-15):
//
// Test p-value
//----------------------------------------------
// 1 SerialOver, r = 0 eps
// 3 CollisionOver, t = 2 1 - eps1
// 5 CollisionOver, t = 3 1 - eps1
// 7 CollisionOver, t = 7 eps
// Identify the summary line
final String testSuiteName = skipToTestU01Summary(resultFile, iter);
// This may not be present if the results are not complete
if (testSuiteName == null) {
// Rewind
while (iter.hasPrevious()) {
iter.previous();
}
updateTestU01ApplicationName(iter, testResult);
return;
}
setTestU01ApplicationName(testResult, testSuiteName);
// Read test results using the entire line except the p-value for the test Id
// Note:
// This will count sub-parts of the same test as distinct failures.
while (iter.hasNext()) {
final String line = iter.next();
final Matcher matcher = TESTU01_TEST_RESULT_PATTERN.matcher(line);
if (matcher.find()) {
testResult.addFailedTest(matcher.group(1).trim());
} else if (findExitCode(testResult, line)) {
return;
}
}
}
/**
* Sets the Test U01 application name using the provide test suite name.
*
* @param testResult Test result.
* @param testSuiteName Test suite name.
*/
private static void setTestU01ApplicationName(TestResult testResult, String testSuiteName) {
testResult.setTestApplicationName("TestU01 (" + testSuiteName + ")");
}
/**
* Skip to the Test U01 result summary.
*
* <p>If this cannot be found the method will throw an exception unless partial results
* are allowed.</p>
*
* @param resultFile Result file (for the exception message).
* @param iter Iterator of the test output.
* @return the name of the test suite
* @throws ApplicationException If the TestU01 summary cannot be found.
*/
private String skipToTestU01Summary(File resultFile, Iterator<String> iter) {
final String testSuiteName = findMatcherGroup1(iter, TESTU01_SUMMARY_PATTERN);
// Allow the partial result to be ignored
if (testSuiteName != null || ignorePartialResults) {
return testSuiteName;
}
throw new ApplicationException("Failed to identify the Test U01 result summary: " + resultFile);
}
/**
* Update the test application name from the Test U01 results. This can be used to identify
* the test suite from the start of the results in the event that the results summary has
* not been output.
*
* @param iter Iterator of the test output.
* @param testResult Test result.
*/
private static void updateTestU01ApplicationName(Iterator<String> iter,
TestResult testResult) {
final String testSuiteName = findMatcherGroup1(iter, TESTU01_STARTING_PATTERN);
if (testSuiteName != null) {
setTestU01ApplicationName(testResult, testSuiteName);
}
}
/**
* Create a matcher for each item in the iterator and if a match is identified return
* group 1.
*
* @param iter Iterator of text to match.
* @param pattern Pattern to match.
* @return the string (or null)
*/
private static String findMatcherGroup1(Iterator<String> iter,
Pattern pattern) {
while (iter.hasNext()) {
final String line = iter.next();
final Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
return matcher.group(1);
}
}
return null;
}
/**
* Read the result output from the PractRand test application.
*
* @param iter Iterator of the test output.
* @param testResult Test result.
*/
private static void readPractRand(Iterator<String> iter,
PractRandTestResult testResult) {
// PractRand results are printed for blocks of byte output that double in size.
// The results report 'unusual' and 'suspicious' output and the test stops
// at the first failure.
// Results are typically reported as the size of output that failed, e.g.
// the JDK random fails at 256KiB.
//
// rng=RNG_stdin32, seed=0xfc6c7332
// length= 128 kilobytes (2^17 bytes), time= 5.9 seconds
// no anomalies in 118 test result(s)
//
// rng=RNG_stdin32, seed=0xfc6c7332
// length= 256 kilobytes (2^18 bytes), time= 7.6 seconds
// Test Name Raw Processed Evaluation
// [Low1/64]Gap-16:A R= +12.4 p = 8.7e-11 VERY SUSPICIOUS
// [Low1/64]Gap-16:B R= +13.4 p = 3.0e-11 FAIL
// [Low8/32]DC6-9x1Bytes-1 R= +7.6 p = 5.7e-4 unusual
// ...and 136 test result(s) without anomalies
testResult.setTestApplicationName("PractRand");
// Store the exponent of the current output length.
int exp = 0;
// Identify any line containing FAIL and then get the test name using
// the first token in the line.
while (iter.hasNext()) {
String line = iter.next();
final Matcher matcher = PRACTRAND_OUTPUT_SIZE_PATTERN.matcher(line);
if (matcher.find()) {
// Store the current output length
exp = Integer.parseInt(matcher.group(1));
} else if (PRACTRAND_FAILED_PATTERN.matcher(line).find()) {
// Record the output length where failures occurred.
testResult.setLengthExponent(exp);
// Add the failed test name. This does not include the length exponent
// allowing meta-processing of systematic failures.
// Remove initial whitespace
line = line.trim();
final int index = line.indexOf(' ');
testResult.addFailedTest(line.substring(0, index));
} else if (findExitCode(testResult, line)) {
return;
}
}
}
/**
* Delete any result file if incomplete.
*
* @param results Results.
* @throws ApplicationException If a file could not be deleted.
*/
private static void deleteIfIncomplete(List<TestResult> results) {
results.forEach(ResultsCommand::deleteIfIncomplete);
}
/**
* Delete the result file if incomplete.
*
* @param result Test result.
* @throws ApplicationException If the file could not be deleted.
*/
private static void deleteIfIncomplete(TestResult result) {
if (!result.isComplete()) {
try {
Files.delete(result.getResultFile().toPath());
LogUtils.info("Deleted file: %s", result.getResultFile());
} catch (IOException ex) {
throw new ApplicationException("Failed to delete file: " + result.getResultFile(), ex);
}
}
}
/**
* Creates the output stream. This will not be buffered.
*
* @return the output stream
*/
private OutputStream createOutputStream() {
if (fileOutput != null) {
try {
Files.newOutputStream(fileOutput.toPath());
} catch (final IOException ex) {
throw new ApplicationException("Failed to create output: " + fileOutput, ex);
}
}
return new FilterOutputStream(System.out) {
@Override
public void close() {
// Do not close stdout
}
};
}
/**
* Write the results to a table in Comma Separated Value (CSV) format.
*
* @param out Output stream.
* @param results Results.
* @throws IOException Signals that an I/O exception has occurred.
*/
private void writeCSVData(OutputStream out,
List<TestResult> results) throws IOException {
// Sort by columns
Collections.sort(results, (o1, o2) -> {
int result = Integer.compare(o1.getRandomSource().ordinal(), o2.getRandomSource().ordinal());
if (result != 0) {
return result;
}
result = Boolean.compare(o1.isBitReversed(), o2.isBitReversed());
if (result != 0) {
return result;
}
result = o1.getTestApplicationName().compareTo(o2.getTestApplicationName());
if (result != 0) {
return result;
}
return Integer.compare(o1.getFailureCount(), o2.getFailureCount());
});
try (BufferedWriter output = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
output.write("RandomSource,Bit-reversed,Test,Failures");
if (showFailedTests) {
output.write(",Failed");
}
output.newLine();
for (final TestResult result : results) {
output.write(result.getRandomSource().toString());
output.write(',');
output.write(Boolean.toString(result.isBitReversed()));
output.write(',');
output.write(result.getTestApplicationName());
output.write(',');
output.write(result.getFailureSummaryString());
// Optionally write out failed test names.
if (showFailedTests) {
output.write(',');
output.write(result.getFailedTests().stream().collect(Collectors.joining("|")));
}
output.newLine();
}
}
}
/**
* Write the results using the Almost Plain Text (APT) format for a table. This
* table can be included in the documentation for the Commons RNG site.
*
* @param out Output stream.
* @param results Results.
* @throws IOException Signals that an I/O exception has occurred.
*/
private void writeAPT(OutputStream out,
List<TestResult> results) throws IOException {
// Identify all:
// RandomSources, bit-reversed, test names,
final List<RandomSource> randomSources = getRandomSources(results);
final List<Boolean> bitReversed = getBitReversed(results);
final List<String> testNames = getTestNames(results);
// Identify the common path prefix to be replaced
final int prefixLength = (pathPrefix.isEmpty()) ? 0 : findCommonPathPrefixLength(results);
// Create a function to update the file path and then output the failure count
// as a link to the file using the APT format.
final Function<TestResult, String> toAPTString = result -> {
String path = result.getResultFile().getPath();
// Remove common path prefix
path = path.substring(prefixLength);
// Build the APT relative link
final StringBuilder sb = new StringBuilder()
.append("{{{").append(pathPrefix).append(path).append('}')
.append(result.getFailureSummaryString()).append("}}");
// Convert to web-link name separators
for (int i = 0; i < sb.length(); i++) {
if (sb.charAt(i) == BACK_SLASH) {
sb.setCharAt(i, FORWARD_SLASH);
}
}
return sb.toString();
};
// Create columns for RandomSource, bit-reversed, each test name.
// Make bit-reversed column optional if no generators are bit reversed.
final boolean showBitReversedColumn = bitReversed.contains(Boolean.TRUE);
final String header = createAPTHeader(showBitReversedColumn, testNames);
final String separator = createAPTSeparator(header);
// Output
try (BufferedWriter output = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
// For the first line using '*' (centre) character instead of '+' (left align)
output.write(separator.replace('+', '*'));
output.write(header);
output.newLine();
output.write(separator);
final StringBuilder sb = new StringBuilder();
// This will collate results for each combination of 'RandomSource + bitReversed'
for (final RandomSource randomSource : randomSources) {
for (final boolean reversed : bitReversed) {
// Highlight in bold a RNG with no systematic failures
boolean highlight = true;
// Buffer the column output
sb.setLength(0);
if (showBitReversedColumn) {
writeAPTColumn(sb, Boolean.toString(reversed), false);
}
for (final String testName : testNames) {
final List<TestResult> testResults = getTestResults(results, randomSource, reversed, testName);
String text = testResults.stream()
.map(toAPTString)
.collect(Collectors.joining(", "));
// Summarise the failures across all tests
final String summary = getFailuresSummary(testResults);
if (summary.length() != 0) {
// Identify RNGs with no systematic failures
highlight = false;
if (showFailedTests) {
// Add summary in brackets
text += " (" + summary + ")";
}
}
writeAPTColumn(sb, text, false);
}
output.write('|');
writeAPTColumn(output, randomSource.toString(), highlight);
output.write(sb.toString());
output.newLine();
output.write(separator);
}
}
}
}
/**
* Gets the random sources present in the results.
*
* @param results Results.
* @return the random sources
*/
private static List<RandomSource> getRandomSources(List<TestResult> results) {
final EnumSet<RandomSource> set = EnumSet.noneOf(RandomSource.class);
for (final TestResult result : results) {
set.add(result.getRandomSource());
}
final ArrayList<RandomSource> list = new ArrayList<>(set);
Collections.sort(list);
return list;
}
/**
* Gets the bit-reversed options present in the results.
*
* @param results Results.
* @return the bit-reversed options
*/
private static List<Boolean> getBitReversed(List<TestResult> results) {
final ArrayList<Boolean> list = new ArrayList<>(2);
if (results.isEmpty()) {
// Default to no bit-reversed results
list.add(Boolean.FALSE);
} else {
final boolean first = results.get(0).isBitReversed();
list.add(first);
for (final TestResult result : results) {
if (first != result.isBitReversed()) {
list.add(!first);
break;
}
}
}
Collections.sort(list);
return list;
}
/**
* Gets the test names present in the results. These are returned in encountered order.
*
* @param results Results.
* @return the test names
*/
private static List<String> getTestNames(List<TestResult> results) {
// Enforce encountered order with a linked hash set.
final Set<String> set = new LinkedHashSet<>();
for (final TestResult result : results) {
set.add(result.getTestApplicationName());
}
return new ArrayList<>(set);
}
/**
* Find the common path prefix for all result files. This is returned as the length of the
* common prefix.
*
* @param results Results.
* @return the length
*/
private static int findCommonPathPrefixLength(List<TestResult> results) {
if (results.isEmpty()) {
return 0;
}
// Find the first prefix
final String prefix1 = getPathPrefix(results.get(0));
int length = prefix1.length();
for (int i = 1; i < results.size() && length != 0; i++) {
final String prefix2 = getPathPrefix(results.get(i));
// Update
final int size = Math.min(prefix2.length(), length);
length = 0;
while (length < size && prefix1.charAt(length) == prefix2.charAt(length)) {
length++;
}
}
return length;
}
/**
* Gets the path prefix.
*
* @param testResult Test result.
* @return the path prefix (or the empty string)
*/
private static String getPathPrefix(TestResult testResult) {
final String parent = testResult.getResultFile().getParent();
return parent == null ? "" : parent;
}
/**
* Creates the APT header.
*
* @param showBitReversedColumn Set to true to the show bit reversed column.
* @param testNames Test names.
* @return the header
*/
private static String createAPTHeader(boolean showBitReversedColumn,
List<String> testNames) {
final StringBuilder sb = new StringBuilder(100).append("|| RNG identifier ||");
if (showBitReversedColumn) {
sb.append(" Bit-reversed ||");
}
for (final String name : testNames) {
sb.append(' ').append(name).append(" ||");
}
return sb.toString();
}
/**
* Creates the APT separator for each table row.
*
* <p>The separator is created using the '+' character to left align the columns.
*
* @param header Header.
* @return the separator
*/
private static String createAPTSeparator(String header) {
// Replace everything with '-' except '|' which is replaced with "*-" for the first
// character, "+-" for all other occurrences except "-+" at the end
final StringBuilder sb = new StringBuilder(header);
for (int i = 0; i < header.length(); i++) {
if (sb.charAt(i) == PIPE) {
sb.setCharAt(i, i == 0 ? '*' : '+');
sb.setCharAt(i + 1, '-');
} else {
sb.setCharAt(i, '-');
}
}
// Fix the end
sb.setCharAt(header.length() - 2, '-');
sb.setCharAt(header.length() - 1, '+');
sb.append(System.lineSeparator());
return sb.toString();
}
/**
* Write the column text to the output.
*
* @param output Output.
* @param text Text.
* @param highlight If {@code true} highlight the text in bold.
* @throws IOException Signals that an I/O exception has occurred.
*/
private static void writeAPTColumn(Appendable output,
String text,
boolean highlight) throws IOException {
output.append(' ');
if (highlight) {
output.append("<<");
}
output.append(text);
if (highlight) {
output.append(">>");
}
output.append(" |");
}
/**
* Gets the test results that match the arguments.
*
* @param results Results.
* @param randomSource Random source.
* @param bitReversed Bit reversed flag.
* @param testName Test name.
* @return the matching results
*/
private static List<TestResult> getTestResults(List<TestResult> results,
RandomSource randomSource,
boolean bitReversed,
String testName) {
final ArrayList<TestResult> list = new ArrayList<>();
for (final TestResult result : results) {
if (result.getRandomSource() == randomSource &&
result.bitReversed == bitReversed &&
result.getTestApplicationName().equals(testName)) {
list.add(result);
}
}
return list;
}
/**
* Gets the systematic failures (tests that fail in every test result).
*
* @param results Results.
* @return the systematic failures
*/
private static List<String> getSystematicFailures(List<TestResult> results) {
final HashMap<String, Integer> map = new HashMap<>();
for (final TestResult result : results) {
// Ignore partial results
if (!result.isComplete()) {
continue;
}
// Some named tests can fail more than once on different statistics.
// For example TestU01 BigCrush LongestHeadRun can output in the summary:
// 86 LongestHeadRun, r = 0 eps
// 86 LongestHeadRun, r = 0 1 - eps1
// This will be counted as 2 failed tests. For the purpose of systematic
// failures the name of the test is the same and should be counted once.
final HashSet<String> unique = new HashSet<>(result.getFailedTests());
for (String test : unique) {
map.merge(test, 1, (i, j) -> i + j);
}
}
final int completeCount = (int) results.stream().filter(TestResult::isComplete).count();
final List<String> list = map.entrySet().stream()
.filter(e -> e.getValue() == completeCount)
.map(Entry::getKey)
.collect(Collectors.toCollection(
(Supplier<List<String>>) ArrayList::new));
// Special case for PractRand. Add the maximum RNG output length before failure.
// This is because some PractRand tests may not be counted as systematic failures
// as they have not been run to the same output length due to earlier failure of
// another test.
final int max = getMaxLengthExponent(results);
if (max != 0) {
list.add(bytesToString(max));
}
return list;
}
/**
* Gets the maximum length exponent from the PractRand results if <strong>all</strong> failed.
* Otherwise return zero (i.e. some passed the full length of the test).
*
* <p>This method excludes those results that are not complete. It assumes all complete
* tests are for the same length of RNG output. Thus if all failed then the max exponent
* is the systematic failure length.</p>
*
* @param results Results.
* @return the maximum length exponent (or zero)
*/
private static int getMaxLengthExponent(List<TestResult> results) {
if (results.isEmpty()) {
return 0;
}
// [0] = count of zeros
// [1] = max non-zero
final int[] data = new int[2];
results.stream()
.filter(TestResult::isComplete)
.filter(r -> r instanceof PractRandTestResult)
.mapToInt(r -> ((PractRandTestResult) r).getLengthExponent())
.forEach(i -> {
if (i == 0) {
// Count results that passed
data[0]++;
} else {
// Find the max of the failures
data[1] = Math.max(i, data[1]);
}
});
// If all failed (i.e. no zeros) then return the max, otherwise zero.
return data[0] == 0 ? data[1] : 0;
}
/**
* Gets a summary of the failures across all results. The text is empty if there are no
* failures to report.
*
* <p>For Dieharder and TestU01 this is the number of systematic failures (tests that fail
* in every test result). For PractRand it is the maximum byte output size that was reached
* before failure.
*
* <p>It is assumed all the results are for the same test suite.</p>
*
* @param results Results.
* @return the failures summary
*/
private static String getFailuresSummary(List<TestResult> results) {
if (results.isEmpty()) {
return "";
}
if (results.get(0).getTestFormat() == TestFormat.PRACTRAND) {
final int max = getMaxLengthExponent(results);
return max == 0 ? "" : bytesToString(max);
}
final int count = getSystematicFailures(results).size();
return count == 0 ? "" : Integer.toString(count);
}
/**
* Write the results as a text table.
*
* @param out Output stream.
* @param results Results.
* @throws IOException Signals that an I/O exception has occurred.
*/
private static void writeTXT(OutputStream out,
List<TestResult> results) throws IOException {
// Identify all:
// RandomSources, bit-reversed, test names,
final List<RandomSource> randomSources = getRandomSources(results);
final List<Boolean> bitReversed = getBitReversed(results);
final List<String> testNames = getTestNames(results);
// Create columns for RandomSource, bit-reversed, each test name.
// Make bit-reversed column optional if no generators are bit reversed.
final boolean showBitReversedColumn = bitReversed.contains(Boolean.TRUE);
final List<List<String>> columns = createTXTColumns(testNames, showBitReversedColumn);
// Add all data
// This will collate results for each combination of 'RandomSource + bitReversed'
for (final RandomSource randomSource : randomSources) {
for (final boolean reversed : bitReversed) {
int i = 0;
columns.get(i++).add(randomSource.toString());
if (showBitReversedColumn) {
columns.get(i++).add(Boolean.toString(reversed));
}
for (final String testName : testNames) {
final List<TestResult> testResults = getTestResults(results, randomSource,
reversed, testName);
columns.get(i++).add(testResults.stream()
.map(TestResult::getFailureSummaryString)
.collect(Collectors.joining(",")));
columns.get(i++).add(getFailuresSummary(testResults));
}
}
}
writeColumns(out, columns);
}
/**
* Creates the columns for the text output.
*
* @param testNames the test names
* @param showBitReversedColumn Set to true to show the bit reversed column
* @return the list of columns
*/
private static List<List<String>> createTXTColumns(final List<String> testNames,
final boolean showBitReversedColumn) {
final ArrayList<List<String>> columns = new ArrayList<>();
columns.add(createColumn(COLUMN_RNG));
if (showBitReversedColumn) {
columns.add(createColumn(BIT_REVERSED));
}
for (final String testName : testNames) {
columns.add(createColumn(testName));
columns.add(createColumn("∩"));
}
return columns;
}
/**
* Creates the column.
*
* @param columnName Column name.
* @return the list
*/
private static List<String> createColumn(String columnName) {
final ArrayList<String> list = new ArrayList<>();
list.add(columnName);
return list;
}
/**
* Creates the text format from column widths.
*
* @param columns Columns.
* @return the text format string
*/
private static String createTextFormatFromColumnWidths(final List<List<String>> columns) {
final StringBuilder sb = new StringBuilder();
try (Formatter formatter = new Formatter(sb)) {
for (int i = 0; i < columns.size(); i++) {
if (i != 0) {
sb.append('\t');
}
formatter.format("%%-%ds", getColumnWidth(columns.get(i)));
}
}
sb.append(System.lineSeparator());
return sb.toString();
}
/**
* Gets the column width using the maximum length of the column items.
*
* @param column Column.
* @return the column width
*/
private static int getColumnWidth(List<String> column) {
int width = 0;
for (final String text : column) {
width = Math.max(width, text.length());
}
return width;
}
/**
* Write the columns as fixed width text to the output.
*
* @param out Output stream.
* @param columns Columns
* @throws IOException Signals that an I/O exception has occurred.
*/
private static void writeColumns(OutputStream out,
List<List<String>> columns) throws IOException {
// Create format using the column widths
final String format = createTextFormatFromColumnWidths(columns);
// Output
try (BufferedWriter output = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
Formatter formatter = new Formatter(output)) {
final int rows = columns.get(0).size();
final Object[] args = new Object[columns.size()];
for (int row = 0; row < rows; row++) {
for (int i = 0; i < args.length; i++) {
args[i] = columns.get(i).get(row);
}
formatter.format(format, args);
}
}
}
/**
* Write the systematic failures as a text table.
*
* @param out Output stream.
* @param results Results.
* @throws IOException Signals that an I/O exception has occurred.
*/
private static void writeFailures(OutputStream out,
List<TestResult> results) throws IOException {
// Identify all:
// RandomSources, bit-reversed, test names,
final List<RandomSource> randomSources = getRandomSources(results);
final List<Boolean> bitReversed = getBitReversed(results);
final List<String> testNames = getTestNames(results);
// Create columns for RandomSource, bit-reversed, each test name.
// Make bit-reversed column optional if no generators are bit reversed.
final boolean showBitReversedColumn = bitReversed.contains(Boolean.TRUE);
final List<List<String>> columns = createFailuresColumns(testNames, showBitReversedColumn);
final AlphaNumericComparator cmp = new AlphaNumericComparator();
// Add all data for each combination of 'RandomSource + bitReversed'
for (final RandomSource randomSource : randomSources) {
for (final boolean reversed : bitReversed) {
for (final String testName : testNames) {
final List<TestResult> testResults = getTestResults(results, randomSource,
reversed, testName);
final List<String> failures = getSystematicFailures(testResults);
if (failures.isEmpty()) {
continue;
}
Collections.sort(failures, cmp);
for (String failed : failures) {
int i = 0;
columns.get(i++).add(randomSource.toString());
if (showBitReversedColumn) {
columns.get(i++).add(Boolean.toString(reversed));
}
columns.get(i++).add(testName);
columns.get(i).add(failed);
}
}
}
}
writeColumns(out, columns);
}
/**
* Creates the columns for the failures output.
*
* @param testNames the test names
* @param showBitReversedColumn Set to true to show the bit reversed column
* @return the list of columns
*/
private static List<List<String>> createFailuresColumns(final List<String> testNames,
final boolean showBitReversedColumn) {
final ArrayList<List<String>> columns = new ArrayList<>();
columns.add(createColumn(COLUMN_RNG));
if (showBitReversedColumn) {
columns.add(createColumn(BIT_REVERSED));
}
columns.add(createColumn("Test Suite"));
columns.add(createColumn("Test"));
return columns;
}
/**
* Convert bytes to a human readable string. The byte size is expressed in powers of 2.
* The output units use the ISO binary prefix for increments of 2<sup>10</sup> or 1024.
*
* <p>This is a utility function used for reporting PractRand output sizes.
* Example output:
*
* <pre>
* exponent Binary
* 0 0 B
* 10 1 KiB
* 13 8 KiB
* 20 1 MiB
* 27 128 MiB
* 30 1 GiB
* 40 1 TiB
* 50 1 PiB
* 60 1 EiB
* </pre>
*
* @param exponent Exponent of the byte size (i.e. 2^exponent).
* @return the string
*/
static String bytesToString(int exponent) {
final int unit = exponent / 10;
final int size = 1 << (exponent - 10 * unit);
return size + BINARY_UNITS[unit];
}
}