ActionReporter.java

package com.github.dakusui.actionunit.visitors;

import com.github.dakusui.actionunit.actions.Composite;
import com.github.dakusui.actionunit.core.Action;
import com.github.dakusui.actionunit.io.Writer;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

public class ActionReporter extends ActionPrinter {
  public static final Predicate<Action>   DEFAULT_CONDITION_TO_SQUASH_ACTION = v -> v instanceof Composite;
  private final       List<Boolean>       failingContext                     = new LinkedList<>();
  private final       Predicate<Action>   conditionToSquashAction;
  private             int                 emptyLevel                         = 0;
  private             int                 depth                              = 0;
  private final       Map<Action, Record> report;
  private final       Writer              warnWriter;
  private final       Writer              traceWriter;
  private final       Writer              debugWriter;
  private final       Writer              infoWriter;
  private final       int                 forcePrintLevelForUnexercisedActions;

  public ActionReporter(Predicate<Action> conditionToSquashAction, Writer warnWriter, Writer infoWriter, Writer debugWriter, Writer traceWriter, Map<Action, Record> report, int forcePrintLevelForUnexercisedActions) {
    super(infoWriter);
    this.conditionToSquashAction = conditionToSquashAction;
    this.report = requireNonNull(report);
    this.warnWriter = requireNonNull(warnWriter);
    this.debugWriter = requireNonNull(debugWriter);
    this.infoWriter = infoWriter;
    this.traceWriter = traceWriter;
    this.forcePrintLevelForUnexercisedActions = forcePrintLevelForUnexercisedActions;
  }

  public ActionReporter(Writer writer, Map<Action, Record> report) {
    this(DEFAULT_CONDITION_TO_SQUASH_ACTION, writer, writer, writer, writer, report, 2);
  }

  public void report(Action action) {
    requireNonNull(action).accept(this);
  }

  @Override
  protected void handleAction(Action action) {
    if (this.conditionToSquashAction.test(action)) {
      this.previousIndent = indent();
      return;
    }
    Record runs = report.get(action);
    String message = format("%s[%s]%s", indent(), runs != null ? runs : "", action);
    this.previousIndent = "";
    if (isInFailingContext()) {
      this.warnWriter.writeLine(message);
    } else {
      if (emptyLevel < 1) { // Top level unexercised + exercised ones
        if (passingLevels() < 1) {
          this.infoWriter.writeLine(message);
        } else {
          writeLineForUnexercisedAction(message);
        }
      } else {
        writeLineForUnexercisedAction(message);
      }
    }
  }

  String previousIndent = "";

  /**
   * //@formatter.off
   * ----
   * > [E:0]for each of (noname) parallely
   * >  +-[EE:0]print1
   * >    |   [EE:0](noname)
   * >    +-[]print2
   * >      |   [](noname)
   * >      +-[]print2-1
   * >      |   [](noname)
   * >      +-[]print2-2
   * >        [](noname)
   * ----
   * //@format:on
   */
  @Override
  public String indent() {
    List<? extends Action> path = this.path();
    StringBuilder b = new StringBuilder();
    if (!path.isEmpty()) {
      Action last = path.get(path.size() - 1);
      for (Action each : path) {
        if (each instanceof Composite) {
          if (each == last) {
            if (((Composite) each).isParallel())
              b.append("*-");
            else
              b.append("+-");
          } else {
            if (isLastChild(nextOf(each, path), each))
              b.append("  ");
            else
              b.append("| ");
          }
        } else {
          b.append("  ");
        }
      }
    }
    return mergeStrings(this.previousIndent, b.toString());
  }

  private static Action nextOf(Action each, List<? extends Action> path) {
    return path.get(path.indexOf(each) + 1);
  }

  /**
   * Merges two string into one.
   * A white space in `a` or `b` will be replaced with non-white space in the other at the same position.
   * In case both have non-white space in the same position, the latter's (`b`) overrides the first's (`a`).
   * <p>
   * .Example input
   * ----
   * a:"hello    "
   * b:"    O WORLD "
   * ----
   * <p>
   * .Example output
   * ----
   * "hellO WORLD "
   * ----
   *
   * @param a A string to be merged.
   * @param b A stringto be merged
   * @return The merged result string.
   */
  private static String mergeStrings(String a, String b) {
    StringBuilder builder = new StringBuilder();
    int min = Math.min(a.length(), b.length());
    for (int i = 0; i < min; i++) {
      char ach = a.charAt(i);
      char bch = b.charAt(i);
      if (bch != ' ')
        builder.append(bch);
      else
        builder.append(ach);
    }
    // Whichever longer, the result is the same since the shorter.substring(min) will become an empty
    // string.
    builder.append(a.substring(min));
    builder.append(b.substring(min));
    return builder.toString();
  }

  private static boolean isLastChild(Action each, Action parent) {
    if (parent instanceof Composite) {
      int index = ((Composite) parent).children().indexOf(each);
      int size = ((Composite) parent).children().size();
      assert index >= 0;
      return index == size - 1;
    }
    return true;
  }

  private void writeLineForUnexercisedAction(String message) {
    // unexercised
    if (depth < this.forcePrintLevelForUnexercisedActions)
      this.debugWriter.writeLine(message);
    else
      this.traceWriter.writeLine(message);
  }

  private int passingLevels() {
    int ret = 0;
    for (boolean each : this.failingContext)
      if (!each)
        ret++;
    return ret;
  }

  boolean isInFailingContext() {
    return !this.failingContext.isEmpty() && this.failingContext.get(0);
  }

  void pushFailingContext(boolean newContext) {
    failingContext.add(0, newContext);
  }

  void popFailingContext() {
    failingContext.remove(0);
  }

  @Override
  protected void enter(Action action) {
    super.enter(action);
    depth++;
    Record runs = report.get(action);
    pushFailingContext(runs != null && runs.allFailing());
    if (runs == null)
      emptyLevel++;

  }

  @Override
  protected void leave(Action action) {
    Record runs = report.get(action);
    if (runs == null)
      emptyLevel--;
    popFailingContext();
    depth--;
    super.leave(action);
  }
  /*
  [E:0]for each of (noname) parallely
  [EE:0]do sequentially
  |  [EE:0]print
  |    [EE:0](noname)
  | []print
  |    [](noname)
  | :[]print
    :   [](noname)
    :[]print
    :   [](noname)

        : []parallel1
        : | | []sequential(1.1)
        : | | []sequential(1.1)
        : | []sequential(2)
        : |[]sequential(1)
        : []parallel2

   */
  /*
[E:0]do sequentially
+-[E:0]do sequentially
  +-[E:0]do sequentially
    +-[E:0]print2-1
    |   [E:0](noname)
    +-[]print2-2
        [](noname)

|
V

+-
  +-
    +-[E:0]print2-1
    |   [E:0](noname)
    +-[]print2-2
        [](noname)

|
V

+-+-+-[E:0]print2-1
    |   [E:0](noname)
    +-[]print2-2
        [](noname)

   */
}