EvaluationEntry.java

package com.github.dakusui.pcond.core;

import com.github.dakusui.pcond.core.ValueHolder.State;
import com.github.dakusui.pcond.validator.Validator;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

import static com.github.dakusui.pcond.core.EvaluationContext.resolveEvaluationEntryType;
import static com.github.dakusui.pcond.core.EvaluationEntry.Type.*;
import static com.github.dakusui.pcond.core.Evaluator.Explainable.*;
import static com.github.dakusui.pcond.core.Evaluator.Impl.EVALUATION_SKIPPED;
import static com.github.dakusui.pcond.core.Evaluator.Snapshottable.toSnapshotIfPossible;
import static com.github.dakusui.pcond.core.ValueHolder.CreatorFormType.FUNC_TAIL;
import static com.github.dakusui.pcond.core.ValueHolder.State.VALUE_RETURNED;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

/**
 *
 * // @formatter:off
 * A class to hold an entry of execution history of the {@link Evaluator}.
 * When an evaluator enters into one {@link Evaluable} (actually a predicate or a function),
 * an {@code OnGoing} entry is created and held by the evaluator as a current
 * one.
 * Since one evaluate can have its children and only one child can be evaluated at once,
 * on-going entries are held as a list (stack).
 *
 * When the evaluator leaves the evaluable, the entry is "finalized".
 * From the data held by an entry, "expectation" and "actual behavior" reports are generated.
 *
 * .Evaluation Summary Format
 * ----
 * +----------------------------------------------------------------------------- Failure Detail Index
 * |  +-------------------------------------------------------------------------- Input
 * |  |                                            +----------------------------- Form (Function/Predicate)
 * |  |                                            |                           +- Output
 * |  |                                            |                           |
 * V  V                                            V                           V
 *     Book:[title:<De Bello G...i appellantur.>]->check:allOf               ->false
 *                                                    transform:title       ->"De Bello Gallico"
 *                       "De Bello Gallico"     ->    check:allOf           ->false
 *                                                        isNotNull         ->true
 * [0]                                                    transform:parseInt->NumberFormatException:"For input s...ico""
 *                          null                ->        check:allOf       ->false
 *                                                            >=[10]        ->true
 *                                                            <[40]         ->true
 *    Book:[title:<De Bello G...i appellantur.>]->    transform:title       ->"Gallia est omnis divis...li appellantur."
 *    "Gallia est omnis divis...li appellantur."->    check:allOf           ->false
 *                                                        isNotNull         ->true
 *                                                        transform:length  ->145
 *    145                                       ->        check:allOf       ->false
 * [1]                                                         >=[200]       ->true
 * <[400]        ->true
 * ----
 *
 * Failure Detail Index::
 * In the full format of a failure report, detailed descriptions of mismatching forms are provided if the form is {@link Evaluator.Explainable}.
 * This index points an item in the detail part of the full report.
 * Input::
 * Values given to forms are printed here.
 * If the previous line uses the same value, the value will not be printed.
 * Form (Function/Predicate)::
 * This part displays names of forms (predicates and functions).
 * If a form is marked trivial, the framework may merge the form with the next line.
 * Output::
 * For predicates, expected boolean value is printed.
 * For functions, if a function does not throw an exception during its evaluation, the result will be printed here both for expectation and actual behavior summary.
 * If it throws an exception, the exception will be printed here in actual behavior summary.
 *
 * // @formatter:on
 */
public abstract class EvaluationEntry {
  private final Type   type;
  /**
   * A name of a form (evaluable; function, predicate)
   */
  private final String formName;
  int level;
  
  Object inputExpectation;
  Object detailInputExpectation;
  
  Object inputActualValue;
  Object detailInputActualValue;
  
  Object outputExpectation;
  Object detailOutputExpectation;
  
  
  /**
   * A flag to let the framework know this entry should be printed in a less outstanding form.
   */
  final boolean squashable;
  
  EvaluationEntry(String formName, Type type, int level, Object inputExpectation_, Object detailInputExpectation_, Object outputExpectation, Object detailOutputExpectation, Object inputActualValue, Object detailInputActualValue, boolean squashable) {
    this.type = type;
    this.level = level;
    this.formName = formName;
    this.inputExpectation = inputExpectation_;
    this.detailInputExpectation = detailInputExpectation_;
    this.outputExpectation = outputExpectation;
    this.detailOutputExpectation = detailOutputExpectation;
    this.inputActualValue = inputActualValue;
    this.detailInputActualValue = detailInputActualValue;
    this.squashable = squashable;
  }
  
  public String formName() {
    return formName;
  }
  
  public Type type() {
    return this.type;
  }
  
  @SuppressWarnings("BooleanMethodIsAlwaysInverted")
  public boolean isSquashable(EvaluationEntry nextEntry) {
    return this.squashable;
  }
  
  public abstract boolean requiresExplanation();
  
  public int level() {
    return level;
  }
  
  public Object inputExpectation() {
    return this.inputExpectation;
  }
  
  public Object detailInputExpectation() {
    return this.detailInputExpectation;
  }
  
  public Object outputExpectation() {
    return this.outputExpectation;
  }
  
  public Object detailOutputExpectation() {
    return this.detailOutputExpectation;
  }
  
  public Object inputActualValue() {
    return this.inputActualValue;
  }
  
  public abstract Object outputActualValue();
  
  public abstract Object detailOutputActualValue();
  
  public abstract boolean ignored();
  
  @Override
  public String toString() {
    return String.format("%s(%s)", formName(), inputActualValue());
  }
  
  static String composeDetailOutputActualValueFromInputAndThrowable(Object input, Throwable throwable) {
    StringBuilder b = new StringBuilder();
    b.append("Input: '").append(input).append("'").append(format("%n"));
    b.append("Input Type: ").append(input == null ? "(null)" : input.getClass().getName()).append(format("%n"));
    b.append("Thrown Exception: '").append(throwable.getClass().getName()).append("'").append(format("%n"));
    b.append("Exception Message: ").append(sanitizeExceptionMessage(throwable)).append(format("%n"));
    
    for (StackTraceElement each : foldInternalPackageElements(throwable)) {
      b.append("\t");
      b.append(each);
      b.append(format("%n"));
    }
    return b.toString();
  }
  
  private static String sanitizeExceptionMessage(Throwable throwable) {
    if (throwable.getMessage() == null)
      return null;
    return Arrays.stream(throwable.getMessage().split("\n"))
        .map(s -> "> " + s)
        .collect(joining(String.format("%n")));
  }
  
  static <T, E extends Evaluable<T>> Object computeInputActualValue(EvaluableIo<T, E, ?> evaluableIo) {
    return evaluableIo.input().value();
  }
  
  static <T, E extends Evaluable<T>> Object computeOutputExpectation(EvaluableIo<T, E, ?> evaluableIo, boolean expectationFlipped) {
    final State state = evaluableIo.output().state();
    if (state == VALUE_RETURNED) {
      if (evaluableIo.evaluableType() == FUNCTION || evaluableIo.evaluableType() == TRANSFORM)
        return toSnapshotIfPossible(evaluableIo.output().returnedValue());
      return !expectationFlipped;
    } else if (state == State.EXCEPTION_THROWN || state == State.EVALUATION_SKIPPED)
      return EVALUATION_SKIPPED;
    else
      throw new AssertionError("output state=<" + state + ">");
  }
  
  static <T, E extends Evaluable<T>> Object computeOutputActualValue(EvaluableIo<T, E, ?> evaluableIo) {
    if (evaluableIo.output().state() == State.VALUE_RETURNED)
      return toSnapshotIfPossible(evaluableIo.output().returnedValue());
    if (evaluableIo.output().state() == State.EXCEPTION_THROWN)
      return evaluableIo.output().thrownException();
    else
      return EVALUATION_SKIPPED;
  }
  
  static <T, E extends Evaluable<T>> boolean isExplanationRequired(EvaluableIo<T, E, ?> evaluableIo, boolean expectationFlipped) {
    return asList(FUNCTION, LEAF).contains(evaluableIo.evaluableType()) && (
        evaluableIo.output().state() == State.EXCEPTION_THROWN || (
            evaluableIo.evaluable() instanceof Evaluable.LeafPred && returnedValueOrVoidIfSkipped(expectationFlipped, evaluableIo)));
  }
  
  private static List<StackTraceElement> foldInternalPackageElements(Throwable throwable) {
    AtomicReference<StackTraceElement> firstInternalStackElement = new AtomicReference<>();
    String lastPackageNameElementPattern = "\\.[a-zA-Z0-9_.]+$";
    String internalPackageName = Validator.class.getPackage().getName()
        .replaceFirst(lastPackageNameElementPattern, "")
        .replaceFirst(lastPackageNameElementPattern, "");
    return Arrays.stream(throwable.getStackTrace())
        .filter(e -> {
          if (e.getClassName().startsWith(internalPackageName)) {
            if (firstInternalStackElement.get() == null) {
              firstInternalStackElement.set(e);
              return true;
            }
            return false;
          }
          firstInternalStackElement.set(null);
          return true;
        })
        .map(e -> {
          if (e.getClassName().startsWith(internalPackageName)) {
            return new StackTraceElement("...internal.package.InternalClass", "internalMethod", "InternalClass.java", 0);
          }
          return e;
        })
        .collect(toList());
  }
  
  private static boolean returnedValueOrVoidIfSkipped(boolean expectationFlipped, EvaluableIo<?, ?, ?> io) {
    if (io.output().state() == State.EVALUATION_SKIPPED)
      return false;
    return expectationFlipped ^ !(Boolean) io.output().returnedValue();
  }
  
  public enum Type {
    TRANSFORM_AND_CHECK {
      @Override
      String formName(Evaluable<?> evaluable) {
        return "transformAndCheck";
      }
    },
    TRANSFORM {
      @Override
      String formName(Evaluable<?> evaluable) {
        return "transform";
      }
      
      @Override
      boolean isSquashableWith(EvaluationEntry.Impl nextEntry) {
        if (Objects.equals(FUNCTION, nextEntry.evaluableIo().evaluableType()))
          return !((Evaluable.Func<?>) nextEntry.evaluableIo().evaluable()).tail().isPresent();
        return false;
      }
    },
    CHECK {
      @Override
      String formName(Evaluable<?> evaluable) {
        return resolveEvaluationEntryType(evaluable).formName(evaluable);
      }
      
      @Override
      boolean isSquashableWith(EvaluationEntry.Impl nextEntry) {
        return asList(LEAF, NOT, AND, OR, TRANSFORM).contains(nextEntry.evaluableIo().evaluableType());
      }
    },
    AND {
      @Override
      String formName(Evaluable<?> evaluable) {
        return ((Evaluable.Conjunction<?>) evaluable).shortcut() ? "and" : "allOf";
      }
    },
    OR {
      @Override
      String formName(Evaluable<?> evaluable) {
        return ((Evaluable.Disjunction<?>) evaluable).shortcut() ? "or" : "anyOf";
      }
    },
    NOT {
      @Override
      String formName(Evaluable<?> evaluable) {
        return "not";
      }
      
      @Override
      boolean isSquashableWith(EvaluationEntry.Impl nextEntry) {
        return Objects.equals(LEAF, nextEntry.evaluableIo().evaluableType());
      }
    },
    LEAF {
      @Override
      String formName(Evaluable<?> evaluable) {
        return evaluable.toString();
      }
    },
    FUNCTION {
      @Override
      String formName(Evaluable<?> evaluable) {
        if (DebuggingUtils.showEvaluableDetail()) {
          if (!((Evaluable.Func<?>) evaluable).tail().isPresent())
            return ((Evaluable.Func<?>) evaluable).head().toString();
          return ((Evaluable.Func<?>) evaluable).head().toString() + "(" + ((Evaluable.Func<?>) evaluable).tail().get() + ")";
        }
        return ((Evaluable.Func<?>) evaluable).head().toString();
      }
    };
    
    abstract String formName(Evaluable<?> evaluable);
    
    boolean isSquashableWith(EvaluationEntry.Impl nextEntry) {
      return false;
    }
  }
  
  static class Finalized extends EvaluationEntry {
    final         Object  outputActualValue;
    final         Object  detailOutputActualValue;
    private final boolean requiresExplanation;
    private final boolean ignored;
    
    Finalized(
        String formName,
        Type type,
        int level,
        Object inputExpectation_, Object detailInputExpectation_,
        Object outputExpectation, Object detailOutputExpectation,
        Object inputActualValue, Object detailInputActualValue,
        Object outputActualValue, Object detailOutputActualValue,
        boolean squashable, boolean requiresExplanation, boolean ignored) {
      super(
          formName, type, level,
          inputExpectation_, detailInputExpectation_,
          outputExpectation, detailOutputExpectation,
          inputActualValue, detailInputActualValue, squashable);
      this.outputActualValue = outputActualValue;
      this.detailOutputActualValue = detailOutputActualValue;
      this.requiresExplanation = requiresExplanation;
      this.ignored = ignored;
    }
    
    @Override
    public Object outputActualValue() {
      return outputActualValue;
    }
    
    @Override
    public Object detailOutputActualValue() {
      return this.detailOutputActualValue;
    }
    
    @Override
    public boolean ignored() {
      return this.ignored;
    }
    
    @Override
    public boolean requiresExplanation() {
      return this.requiresExplanation;
    }
  }
  
  public static EvaluationEntry create(
      String formName, Type type,
      int level,
      Object inputExpectation_, Object detailInputExpectation_,
      Object outputExpectation, Object detailOutputExpectation,
      Object inputActualValue, Object detailInputActualValue,
      Object outputActualValue, Object detailOutputActualValue,
      boolean trivial, boolean requiresExplanation, boolean ignored) {
    return new Finalized(
        formName, type,
        level,
        inputExpectation_, detailInputExpectation_,
        outputExpectation, detailOutputExpectation,
        inputActualValue, detailInputActualValue,
        outputActualValue, detailOutputActualValue,
        trivial, requiresExplanation, ignored
    );
  }
  
  public static class Impl extends EvaluationEntry {
    
    private final EvaluableIo<?, ?, ?> evaluableIo;
    private final boolean              expectationFlipped;
    private       boolean              ignored;
    
    private boolean finalized = false;
    private Object  outputActualValue;
    private Object  detailOutputActualValue;
    
    <T, E extends Evaluable<T>> Impl(
        EvaluationContext<T> evaluationContext,
        EvaluableIo<T, E, ?> evaluableIo) {
      super(
          EvaluationContext.formNameOf(evaluableIo),
          evaluableIo.evaluableType(),
          evaluationContext.visitorLineage.size(),
          computeInputExpectation(evaluableIo),                   // inputExpectation        == inputActualValue
          explainInputExpectation(evaluableIo),                   // detailInputExpectation  == detailInputActualValue
          null, // not necessary                                  // outputExpectation
          explainOutputExpectation(evaluableIo.evaluable(), evaluableIo),      // detailOutputExpectation
          computeInputActualValue(evaluableIo),                   // inputActualValue
          explainInputActualValue(evaluableIo.evaluable(), computeInputActualValue(evaluableIo)), // detailInputActualValue
          evaluableIo.evaluable().isSquashable());
      this.evaluableIo = evaluableIo;
      this.expectationFlipped = evaluationContext.isExpectationFlipped();
      this.ignored = false;
    }
    
    private static <E extends Evaluable<T>, T> Object explainInputExpectation(EvaluableIo<T, E, ?> evaluableIo) {
      return explainInputActualValue(evaluableIo, computeInputExpectation(evaluableIo));
    }
    
    private static <E extends Evaluable<T>, T> Object computeInputExpectation(EvaluableIo<T, E, ?> evaluableIo) {
      return computeInputActualValue(evaluableIo);
    }
    
    @Override
    public boolean requiresExplanation() {
      return isExplanationRequired(evaluableIo(), this.expectationFlipped);
    }
    
    @SuppressWarnings("unchecked")
    public <I, O> EvaluableIo<I, Evaluable<I>, O> evaluableIo() {
      return (EvaluableIo<I, Evaluable<I>, O>) this.evaluableIo;
    }
    
    public Object outputExpectation() {
      assert finalized;
      return outputExpectation;
    }
    
    @Override
    public Object outputActualValue() {
      assert finalized;
      return outputActualValue;
    }
    
    @Override
    public Object detailOutputActualValue() {
      assert finalized;
      return detailOutputActualValue;
    }
    
    public boolean ignored() {
      assert finalized;
      return this.ignored;
    }
    
    public boolean isSquashable(EvaluationEntry nextEntry) {
      if (nextEntry instanceof EvaluationEntry.Impl)
        return this.type().isSquashableWith((Impl) nextEntry);
      return false;
    }
    
    public String formName() {
      if (DebuggingUtils.showEvaluableDetail())
        return evaluableIo.formName() + "(" +
            evaluableIo.evaluableType() + ":" +
            evaluableIo.input().creatorFormType() + ":" +
            evaluableIo.output().creatorFormType() +
            (finalized && this.ignored() ? ":ignored" : "") + ")";
      return this.evaluableIo.formName();
    }
    
    public void finalizeValues() {
      this.outputExpectation = computeOutputExpectation(evaluableIo(), expectationFlipped);
      this.outputActualValue = computeOutputActualValue(evaluableIo());
      this.detailOutputActualValue = explainActual(evaluableIo());
      this.ignored =
          (this.evaluableIo.evaluableType() == TRANSFORM_AND_CHECK && this.evaluableIo.formName().equals("transformAndCheck")) ||
              (this.evaluableIo.evaluableType() == FUNCTION && this.evaluableIo.output().creatorFormType() == FUNC_TAIL);
      this.finalized = true;
    }
    
    @Override
    public String toString() {
      return String.format("%s(%s)=%s (expected:=%s):%s", formName(), inputActualValue(), finalized ? outputActualValue() : "(n/a)", finalized ? outputExpectation() : "(n/a)", this.level());
    }
  }
}