ReportComposer.java
package com.github.dakusui.pcond.validator;
import com.github.dakusui.pcond.core.DebuggingUtils;
import com.github.dakusui.pcond.core.EvaluationEntry;
import com.github.dakusui.pcond.fluent.ValueHolder;
import com.github.dakusui.pcond.internals.InternalUtils;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;
import static com.github.dakusui.pcond.internals.InternalUtils.*;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
public interface ReportComposer {
default Explanation explanationFromMessage(String msg) {
return Explanation.fromMessage(msg);
}
default Explanation composeExplanation(String message, List<EvaluationEntry> evaluationEntries) {
return Utils.composeExplanation(this, message, evaluationEntries);
}
default FormattedEntry createFormattedEntryForExpectation(EvaluationEntry evaluationEntry) {
return Utils.createFormattedEntryForExpectation(this, evaluationEntry);
}
default FormattedEntry createFormattedEntryForActualValue(EvaluationEntry evaluationEntry) {
return Utils.createFormattedEntryForActualValue(this, evaluationEntry);
}
default boolean requiresExplanation(EvaluationEntry evaluationEntry) {
return evaluationEntry.requiresExplanation();
}
/**
* A default implementation of `ReportComposer`.
*/
class Default implements ReportComposer {
}
interface Report {
String summary();
List<String> details();
static Report create(String summary, List<String> details) {
List<String> detailsCopy = unmodifiableList(new ArrayList<>(details));
return new Report() {
@Override
public String summary() {
return summary;
}
@Override
public List<String> details() {
return detailsCopy;
}
};
}
}
class FormattedEntry {
private final String input;
private final String formName;
private final String indent;
private final String output;
private final boolean requiresExplanation;
public FormattedEntry(String input, String formName, String indent, String output, boolean requiresExplanation) {
this.input = input;
this.formName = formName;
this.indent = indent;
this.output = output;
this.requiresExplanation = requiresExplanation;
}
Optional<String> input() {
return Optional.ofNullable(this.input);
}
String indent() {
return this.indent;
}
String formName() {
return this.formName;
}
Optional<String> output() {
return Optional.ofNullable(this.output);
}
public boolean requiresExplanation() {
return this.requiresExplanation;
}
}
enum Utils {
;
/**
* Note that an exception thrown during an evaluation is normally caught by the framework.
*
* @param message A message to be prepended to a summary.
* @param evaluationHistory An "evaluation history" object represented as a list of evaluation entries.
* @return An explanation object.
*/
public static Explanation composeExplanation(ReportComposer reportComposer, String message, List<EvaluationEntry> evaluationHistory) {
List<Object> detailsForExpectation = new LinkedList<>();
List<ReportComposer.FormattedEntry> summaryDataForExpectations = squashTrivialEntries(reportComposer, evaluationHistory)
.stream()
.peek((EvaluationEntry each) -> addToDetailsListIfExplanationIsRequired(reportComposer, detailsForExpectation, each, each::detailOutputExpectation))
.map(reportComposer::createFormattedEntryForExpectation)
.collect(toList());
String textSummaryForExpectations = composeSummaryForExpectations(minimizeIndentation(summaryDataForExpectations));
List<Object> detailsForActual = new LinkedList<>();
List<ReportComposer.FormattedEntry> summaryForActual = squashTrivialEntries(reportComposer, evaluationHistory)
.stream()
.peek((EvaluationEntry each) -> addToDetailsListIfExplanationIsRequired(reportComposer, detailsForActual, each, each::detailOutputActualValue))
.map(reportComposer::createFormattedEntryForActualValue)
.collect(toList());
String textSummaryForActualResult = composeSummaryForActualResults(minimizeIndentation(summaryForActual));
return new Explanation(message,
composeReport(textSummaryForExpectations, detailsForExpectation),
composeReport(textSummaryForActualResult, detailsForActual));
}
public static ReportComposer.FormattedEntry createFormattedEntryForExpectation(ReportComposer reportComposer, EvaluationEntry entry) {
return new ReportComposer.FormattedEntry(
formatObject(entry.inputExpectation()),
entry.formName(),
indent(entry.level()),
formatObject(entry.outputExpectation()),
reportComposer.requiresExplanation(entry));
}
public static ReportComposer.FormattedEntry createFormattedEntryForActualValue(ReportComposer reportComposer, EvaluationEntry entry) {
return new ReportComposer.FormattedEntry(
formatObject(entry.inputActualValue()),
entry.formName(),
indent(entry.level()),
formatObject(entry.outputActualValue()),
reportComposer.requiresExplanation(entry));
}
private static List<FormattedEntry> minimizeIndentation(List<FormattedEntry> summaryForActual) {
String minIndent = summaryForActual.stream()
.map(e -> e.indent)
.min(Comparator.comparingInt(String::length))
.orElse("");
return summaryForActual.stream()
.map(e -> new ReportComposer.FormattedEntry(e.input, e.formName(), e.indent().replaceFirst(minIndent, ""), e.output, e.requiresExplanation()))
.collect(toList());
}
private static List<EvaluationEntry> squashTrivialEntries(ReportComposer reportComposer, List<EvaluationEntry> evaluationHistory) {
if (evaluationHistory.size() > 1) {
List<EvaluationEntry> ret = new LinkedList<>();
List<EvaluationEntry> entriesToSquash = new LinkedList<>();
AtomicReference<EvaluationEntry> cur = new AtomicReference<>();
evaluationHistory.stream()
.filter(each -> !each.ignored() || DebuggingUtils.reportIgnoredEntries())
.filter(each -> {
if (cur.get() != null)
return true;
else {
cur.set(each);
return false;
}
})
.forEach(each -> {
if (entriesToSquash.isEmpty()) {
if (cur.get().isSquashable(each) && !suppressSquashing()) {
entriesToSquash.add(cur.get());
} else {
ret.add(cur.get());
}
} else {
entriesToSquash.add(cur.get());
ret.add(squashEntries(reportComposer, entriesToSquash));
entriesToSquash.clear();
}
cur.set(each);
});
finishLeftOverEntries(reportComposer, ret, entriesToSquash, cur);
return ret.stream()
.filter(e -> !(e.inputActualValue() instanceof ValueHolder))
.collect(toList());
} else {
return new ArrayList<>(evaluationHistory);
}
}
private static void finishLeftOverEntries(ReportComposer reportComposer, List<EvaluationEntry> out, List<EvaluationEntry> leftOverEntriesToSquash, AtomicReference<EvaluationEntry> leftOver) {
if (!leftOverEntriesToSquash.isEmpty() && leftOverEntriesToSquash.get(leftOverEntriesToSquash.size() - 1).isSquashable(leftOver.get()) && !suppressSquashing()) {
leftOverEntriesToSquash.add(leftOver.get());
out.add(squashEntries(reportComposer, leftOverEntriesToSquash));
} else {
if (!leftOverEntriesToSquash.isEmpty())
out.add(squashEntries(reportComposer, leftOverEntriesToSquash));
out.add(leftOver.get());
}
}
private static EvaluationEntry squashEntries(ReportComposer reportComposer, List<EvaluationEntry> squashedItems) {
EvaluationEntry first = squashedItems.get(0);
return EvaluationEntry.create(
squashedItems.stream()
.map(e -> (EvaluationEntry.Impl) e)
.map(EvaluationEntry::formName)
.collect(joining(":")),
first.type(),
first.level(),
first.inputExpectation(), first.detailInputExpectation(),
first.outputExpectation(), computeDetailOutputExpectationFromSquashedItems(squashedItems),
first.inputActualValue(), null,
first.outputActualValue(), squashedItems.get(squashedItems.size() - 1).detailOutputActualValue(),
false,
squashedItems.stream().anyMatch(reportComposer::requiresExplanation), false);
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private static boolean suppressSquashing() {
return DebuggingUtils.suppressSquashing();
}
private static String computeDetailOutputExpectationFromSquashedItems(List<EvaluationEntry> squashedItems) {
return squashedItems.stream()
.filter(e -> e.type() != EvaluationEntry.Type.TRANSFORM && e.type() != EvaluationEntry.Type.CHECK)
.map(EvaluationEntry::detailOutputExpectation)
.map(Objects::toString)
.collect(joining(":"));
}
private static void addToDetailsListIfExplanationIsRequired(ReportComposer reportComposer, List<Object> detailsForExpectation, EvaluationEntry evaluationEntry, Supplier<Object> detailOutput) {
if (reportComposer.requiresExplanation(evaluationEntry))
detailsForExpectation.add(detailOutput.get());
}
static Report composeReport(String summary, List<Object> details) {
List<String> stringFormDetails = details != null ?
details.stream()
.filter(Objects::nonNull)
.map(Objects::toString)
.collect(toList()) :
emptyList();
return ReportComposer.Report.create(summary, stringFormDetails);
}
private static String composeSummaryForActualResults(List<ReportComposer.FormattedEntry> formattedEntries) {
return composeSummary(formattedEntries);
}
private static String composeSummaryForExpectations(List<ReportComposer.FormattedEntry> formattedEntries) {
return composeSummaryForActualResults(formattedEntries);
}
private static String composeSummary(List<ReportComposer.FormattedEntry> formattedEntries) {
AtomicInteger mismatchExplanationCount = new AtomicInteger(0);
boolean mismatchExplanationFound = formattedEntries
.stream()
.anyMatch(ReportComposer.FormattedEntry::requiresExplanation);
return evaluatorEntriesToString(
hideInputValuesWhenRepeated(formattedEntries),
columnLengths -> formattedEntryToString(
columnLengths[0],
columnLengths[1],
columnLengths[2],
mismatchExplanationCount,
mismatchExplanationFound));
}
private static Function<ReportComposer.FormattedEntry, String> formattedEntryToString(
int inputColumnWidth,
int formNameColumnLength,
int outputColumnLength,
AtomicInteger i,
boolean mismatchExplanationFound) {
return (ReportComposer.FormattedEntry formattedEntry) ->
(mismatchExplanationFound ?
format("%-4s", formattedEntry.requiresExplanation ?
"[" + i.getAndIncrement() + "]" : "") :
"") +
format("%-" + max(2, inputColumnWidth) + "s" +
"%-" + (formNameColumnLength + 2) + "s" +
"%-" + max(2, outputColumnLength) + "s",
formattedEntry.input().orElse(""),
formattedEntry.input()
.map(v -> "->")
.orElse(" ") + formatObject(InternalUtils.toNonStringObject(formattedEntry.indent() + formattedEntry.formName()), formNameColumnLength - 2),
formattedEntry
.output()
.map(v -> "->" + v).orElse(""));
}
private static String evaluatorEntriesToString(List<ReportComposer.FormattedEntry> formattedEntries, Function<int[], Function<ReportComposer.FormattedEntry, String>> formatterFactory) {
int maxInputLength = 0, maxIndentAndFormNameLength = 0, maxOutputLength = 0;
for (ReportComposer.FormattedEntry eachEntry : formattedEntries) {
int inputLength = eachEntry.input().map(String::length).orElse(0);
if (inputLength > maxInputLength)
maxInputLength = inputLength;
int inputAndFormNameLength = eachEntry.indent().length() + eachEntry.formName().length();
if (inputAndFormNameLength > maxIndentAndFormNameLength)
maxIndentAndFormNameLength = inputAndFormNameLength;
int outputLength = eachEntry.output().map(String::length).orElse(0);
if (outputLength > maxOutputLength)
maxOutputLength = outputLength;
}
int formNameColumnLength = (formNameColumnLength = max(
DebuggingUtils.showEvaluableDetail() ? 80 : 12,
min(summarizedStringLength(), maxIndentAndFormNameLength))) + formNameColumnLength % 2;
Function<ReportComposer.FormattedEntry, String> formatter = formatterFactory.apply(
new int[] { maxInputLength, formNameColumnLength, maxOutputLength });
return formattedEntries
.stream()
.map(formatter)
.map(s -> ("+" + s).trim().substring(1))
.collect(joining(format("%n")));
}
private static List<ReportComposer.FormattedEntry> hideInputValuesWhenRepeated(List<ReportComposer.FormattedEntry> formattedEntries) {
AtomicReference<Object> previousInput = new AtomicReference<>();
return formattedEntries.stream()
.map(each -> {
if (!Objects.equals(previousInput.get(), each.input())) {
previousInput.set(each.input());
return each;
} else {
return new ReportComposer.FormattedEntry("", each.formName(), each.indent(), each.output().orElse(null), each.requiresExplanation());
}
})
.collect(toList());
}
}
}