
com.github.dakusui.crest.core.Session Maven / Gradle / Ivy
package com.github.dakusui.crest.core;
import com.github.dakusui.crest.functions.TransformingPredicate;
import java.util.*;
import java.util.function.*;
import static com.github.dakusui.crest.utils.InternalUtils.*;
import static java.lang.String.format;
import static java.util.Arrays.asList;
public interface Session {
static void perform(String message, T value, Matcher super T> matcher, ExceptionFactory exceptionFactory) {
Report report = perform(value, matcher);
if (!report.isSuccessful()) {
if (report.exceptions().isEmpty()) {
Throwable exception = exceptionFactory.create(message, report, report.exceptions());
if (exception instanceof RuntimeException)
throw (RuntimeException) exception;
if (exception instanceof Error)
throw (Error) exception;
throw new RuntimeException(exception);
}
throw new ExecutionFailure(message, report.expectation(), report.mismatch(), report.exceptions());
}
}
static Report perform(T value, Matcher matcher) {
return perform(value, matcher, create());
}
static Report perform(T value, Matcher matcher, Session session) {
if (matcher.matches(value, session, new LinkedList<>())) {
session.matched(true);
} else {
matcher.describeExpectation(session.matched(false));
matcher.describeMismatch(value, session);
}
return session.report();
}
Report report();
void describeExpectation(Matcher.Composite matcher);
void describeExpectation(Matcher.Leaf matcher);
void describeMismatch(T value, Matcher.Composite matcher);
void describeMismatch(T value, Matcher.Leaf matcher);
@SuppressWarnings("unchecked")
default boolean matches(Matcher.Leaf leaf, T value, Consumer listener) {
if (this instanceof Impl)
((Impl) this).snapshot(value, null, value);
try {
return this.test(
(Predicate) leaf.p(),
this.apply(
(Function) leaf.func(),
value
));
} catch (RuntimeException | Error exception) {
listener.accept(exception);
addException(exception);
return false;
}
}
O apply(Function func, I value);
boolean test(Predicate pred, I value);
static Session create() {
return new Impl<>();
}
Session addException(Throwable exception);
Session matched(boolean b);
@FunctionalInterface
interface ExceptionFactory {
Throwable create(String message, Report report, List causes);
}
class Impl implements Session {
class Writer {
private int level = 0;
private List buffer = new LinkedList<>();
Impl.Writer enter() {
level++;
return this;
}
Impl.Writer leave() {
level--;
return this;
}
Impl.Writer appendLine(String format, Object... args) {
buffer.add(String.format(indent(this.level) + format, args));
return this;
}
String write() {
return String.join("\n", this.buffer);
}
private String indent(int level) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < level; i++) {
builder.append(" ");
}
return builder.toString();
}
}
private static final String VARIABLE_NAME = "x";
private static final String TRANSFORMED_VARIABLE_NAME = "y";
private Map memoizationMapForFunctions = new HashMap<>();
private Map memoizationMapForPredicates = new HashMap<>();
private Map, String> snapshots = new HashMap<>();
private HashSet> explained = new HashSet<>();
Impl.Writer expectationWriter = new Impl.Writer();
Impl.Writer mismatchWriter = new Impl.Writer();
private boolean result;
private List exceptions = new LinkedList<>();
@Override
public Report report() {
if (!Impl.this.exceptions.isEmpty())
mismatchWriter.appendLine("FAILED");
return new Report() {
private List exceptions = Collections.unmodifiableList(Impl.this.exceptions);
private boolean result = Impl.this.result;
private String mismatch = mismatchWriter.write();
private String expectation = expectationWriter.write();
@Override
public String expectation() {
return expectation;
}
@Override
public String mismatch() {
return mismatch;
}
@Override
public List exceptions() {
return this.exceptions;
}
@Override
public boolean isSuccessful() {
return result && exceptions().isEmpty();
}
};
}
@Override
public void describeExpectation(Matcher.Composite matcher) {
beginExpectation(matcher);
try {
matcher.children().forEach(each -> each.describeExpectation(this));
} finally {
endExpectation(matcher);
}
}
void beginExpectation(Matcher.Composite matcher) {
expectationWriter.appendLine(format("%s:[", matcher.name())).enter();
}
@SuppressWarnings("unused")
void endExpectation(Matcher.Composite matcher) {
expectationWriter.leave().appendLine("]");
}
@Override
public void describeMismatch(T value, Matcher.Composite matcher) {
beginMismatch(value, matcher);
try {
matcher.children().forEach(each -> each.describeMismatch(value, this));
} finally {
endMismatch(value, matcher);
}
}
@Override
public void describeExpectation(Matcher.Leaf matcher) {
describeExpectationTo(expectationWriter, matcher);
}
@SuppressWarnings("unchecked")
@Override
public void describeMismatch(T value, Matcher.Leaf matcher) {
if (this.matches(matcher, value, NOP)) {
describeExpectationTo(mismatchWriter, matcher);
return;
}
Function func = matcher.func();
Predicate> p = matcher.p();
appendMismatchSummary(value, func, p);
// if p is plain predicate
// p(func(x)) == true
// -> In this case, no additional information can be printed for p
// if p is transforming predicate
// p(y) == true
// y = f(func(x))
// -> In this case, how p worked can be broken down into p(y) side and
// f(func(x)) side.
this.mismatchWriter.enter();
this.mismatchWriter.appendLine("%s=%s", VARIABLE_NAME, snapshotOf(null, value));
this.mismatchWriter.leave();
if (p instanceof TransformingPredicate && !fails(func, value)) {
TransformingPredicate pp = (TransformingPredicate) p;
this.mismatchWriter
.enter()
.appendLine(
"%s%s %s",
TRANSFORMED_VARIABLE_NAME,
pp.function(),
pp.predicate())
.leave();
explainFunction(
(T) apply(func, value),
pp.function(),
TRANSFORMED_VARIABLE_NAME, this.mismatchWriter);
this.mismatchWriter
.enter()
.appendLine(
"%s=%s%s",
TRANSFORMED_VARIABLE_NAME,
VARIABLE_NAME,
func);
try {
// This doesn't give additional information if func isn't a chained function
// but still makes easier to read the output.
explainFunction(value, func, VARIABLE_NAME, this.mismatchWriter);
} finally {
this.mismatchWriter.leave();
}
} else {
this.mismatchWriter
.enter()
.appendLine(
"%s%s %s",
VARIABLE_NAME,
func,
p)
.leave();
explainFunction(value, func, VARIABLE_NAME, this.mismatchWriter);
}
}
private void appendMismatchSummary(T value, Function func, Predicate> p) {
String formattedExpectation = formatExpectation(p, func);
String formattedFunction = formatFunction(func, VARIABLE_NAME);
String formattedFunctionOutput = this.snapshotOf(func, value);
if (fails(func, value)) {
this.mismatchWriter.appendLine(
"%s failed with %s",
formattedExpectation,
formattedFunctionOutput
);
} else if (fails(p, this.apply(func, value))) {
this.mismatchWriter.appendLine(
"%s failed with %s",
formattedExpectation,
this.snapshotOf(p, this.apply(func, value))
);
} else {
if (p instanceof TransformingPredicate) {
TransformingPredicate pp = (TransformingPredicate) p;
this.mismatchWriter.appendLine(
"%s was not met because (%s=%s)%s=%s",
formattedExpectation,
formattedFunction,
formattedFunctionOutput,
pp.function(),
this.snapshotOf(pp.function(), this.apply(func, value))
);
} else {
this.mismatchWriter.appendLine(
"%s was not met because %s=%s",
formattedExpectation,
formattedFunction,
formattedFunctionOutput
);
}
}
}
void describeExpectationTo(Impl.Writer writer, Matcher.Leaf matcher) {
writer.appendLine("%s", formatExpectation(matcher.p(), matcher.func()));
}
void beginMismatch(T value, Matcher.Composite matcher) {
if (matcher.isTopLevel())
this.mismatchWriter.appendLine("when %s=%s; then %s:[", VARIABLE_NAME, formatValue(value), matcher.name());
else
this.mismatchWriter.appendLine("%s:[", matcher.name());
mismatchWriter.enter();
}
void endMismatch(T value, Matcher.Composite matcher) {
this.mismatchWriter.leave().appendLine("]->%s", matcher.matches(value, this, new LinkedList<>()));
}
@Override
public Session addException(Throwable exception) {
this.exceptions.add(exception);
return this;
}
@SuppressWarnings("unchecked")
private boolean fails(Function func, Object value) {
try {
this.apply(func, value);
return false;
} catch (Throwable t) {
return true;
}
}
@SuppressWarnings("unchecked")
private boolean fails(Predicate p, Object value) {
try {
this.test(p, value);
return false;
} catch (Throwable t) {
return true;
}
}
/**
* During the assertion process invoked by {@code perform} method, {@code apply}
* method of {@code Function}s held by the matcher to be performed must not
* be called directly but through this method.
*
* @param func A function to be applied.
* @param value A value given to {@code func}.
* @param A type of {@code value} given to {@code func}.
* @param A type of {@code value} returned from {@code func}
* @return Result of {@code func} with {@code value}.
*/
@SuppressWarnings("unchecked")
@Override
public O apply(Function func, I value) {
Object ret = null;
try {
if (func instanceof Call.ChainedFunction) {
Call.ChainedFunction cf = (Call.ChainedFunction) func;
if (cf.previous() != null) {
ret = apply(cf.chained(), apply(cf.previous(), value));
return (O) ret;
}
}
ret = memoizedFunction(func).apply(value);
return (O) ret;
} catch (Throwable e) {
ret = e;
throw rethrow(e);
} finally {
snapshot(ret, func, value);
}
}
/**
* During the assertion process invoked by {@code perform} method, {@code test}
* method of {@code Predicate}s held by the matcher to be performed must not
* be called directly but through this method.
*
* @param pred A predicate to be applied.
* @param value A value given to {@code pred}.
* @param A type of {@code value} given to {@code pred}.
* @return Result of {@code pred} with {@code value}.
*/
@SuppressWarnings("unchecked")
@Override
public boolean test(Predicate pred, I value) {
Object ret = null;
try {
if (pred instanceof TransformingPredicate) {
Function func = ((TransformingPredicate) pred).function();
ret = test(((TransformingPredicate) pred).predicate(), apply(func, value));
return (boolean) ret;
}
ret = memoizedPredicate(pred).test(value);
return (boolean) ret;
} catch (Throwable e) {
ret = e;
throw rethrow(e);
} finally {
snapshot(ret, pred, value);
}
}
@Override
public Session matched(boolean b) {
this.result = b;
return this;
}
@SuppressWarnings("unchecked")
private Function memoizedFunction(Function function) {
return memoizationMapForFunctions.computeIfAbsent(function, this::memoize);
}
@SuppressWarnings("unchecked")
private Predicate memoizedPredicate(Predicate p) {
return memoizationMapForPredicates.computeIfAbsent(p, this::memoize);
}
private Function memoize(Function function) {
Map> memo = new HashMap<>();
return (I i) -> memo.computeIfAbsent(i,
(I j) -> {
try {
O result = function.apply(j);
return () -> result;
} catch (RuntimeException | Error e) {
return () -> {
if (e instanceof RuntimeException)
throw (RuntimeException) e;
throw (Error) e;
};
}
}
).get();
}
private Predicate memoize(Predicate predicate) {
Map memo = new HashMap<>();
return (I i) -> memo.computeIfAbsent(i,
(I j) -> {
try {
boolean result = predicate.test(j);
return () -> result;
} catch (RuntimeException | Error e) {
return () -> {
if (e instanceof RuntimeException)
throw (RuntimeException) e;
throw (Error) e;
};
}
}
).getAsBoolean();
}
private boolean isAlreadyExplained(T value, Function func, String variableName) {
return this.explained.contains(asList(value, func, variableName));
}
private void explained(T value, Function func, String variableName) {
this.explained.add(asList(value, func, variableName));
}
@SuppressWarnings("unchecked")
private void explainFunction(T value, Function func, String variableName, Impl.Writer writer) {
if (func instanceof Call.ChainedFunction) {
if (isAlreadyExplained(value, func, variableName)) {
writer.enter().appendLine("%s%s=(EXPLAINED)", variableName, func).leave();
return;
}
explainChainedFunction(value, (Call.ChainedFunction) func, variableName, writer);
} else {
writer.enter();
try {
writer.appendLine("%s%s=%s", variableName, func, this.snapshotOf(func, value));
} finally {
writer.leave();
}
}
explained(value, func, variableName);
}
@SuppressWarnings("unchecked")
private void explainChainedFunction(I value, Call.ChainedFunction chained, String variableName, Impl.Writer writer) {
writer.enter();
try {
class Entry {
private final String formattedFunctionName;
private final String snapshot;
private Entry(String funcName, String snapshot) {
this.formattedFunctionName = funcName;
this.snapshot = snapshot;
}
}
List workEntries = new LinkedList<>();
for (Call.ChainedFunction c = chained; c != null; c = c.previous()) {
workEntries.add(0, new Entry(
formatFunction(c, variableName),
snapshotOf(c, value)
));
}
List work = new LinkedList<>();
String previousReplacement = "";
for (Entry entry : workEntries) {
String formattedFunctionName = entry.formattedFunctionName;
String replacement = previousReplacement + spaces(formattedFunctionName.length() - previousReplacement.length() - 1);
work.add(String.format(
"%s+-%s%s",
replacement,
times('-', workEntries.get(workEntries.size() - 1).formattedFunctionName.length() - formattedFunctionName.length()),
entry.snapshot
));
previousReplacement = replacement + "|";
work.add(previousReplacement);
}
Collections.reverse(work);
work.forEach(e -> mismatchWriter.appendLine("%s", e));
} finally {
writer.leave();
}
}
private void snapshot(Object out, Object funcOrPredicate, Object value) {
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy