All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 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 key = asList(funcOrPredicate, value);
      if (!snapshots.containsKey(key)) {
        if (out instanceof String) {
          snapshots.put(key, String.format("%s", formatValue(out)));
        } else {
          snapshots.put(key, String.format("%s:%s", formatValue(out), toSimpleClassName(out)));
        }
      }
    }

    private  String snapshotOf(Object funcOrPred, I value) {
      return this.snapshots.get(asList(funcOrPred, value));
    }

    private static String formatExpectation(Predicate p, Function function) {
      if (p instanceof TransformingPredicate) {
        TransformingPredicate pp = (TransformingPredicate) p;
        return String.format("(%s=%s%s)%s %s", TRANSFORMED_VARIABLE_NAME, VARIABLE_NAME, function, pp.function(), pp.predicate());
      } else
        return format("%s %s", formatFunction(function, VARIABLE_NAME), p);
    }
  }
}