ActionReporter.java

  1. package com.github.dakusui.actionunit.visitors;

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

  5. import java.util.LinkedList;
  6. import java.util.List;
  7. import java.util.Map;
  8. import java.util.function.Predicate;

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

  11. /**
  12.  * A basic action reporter which writes an action tree and its report to given writers
  13.  *
  14.  * @see ReportingActionPerformer
  15.  */
  16. public class ActionReporter extends ActionPrinter {
  17.   public static final Predicate<Action>   DEFAULT_CONDITION_TO_SQUASH_ACTION = v -> v instanceof Composite;
  18.   private final       List<Boolean>       failingContext                     = new LinkedList<>();
  19.   private final       Predicate<Action>   conditionToSquashAction;
  20.   private             int                 emptyLevel                         = 0;
  21.   private             int                 depth                              = 0;
  22.   private final       Map<Action, Record> report;
  23.   private final       Writer              warnWriter;
  24.   private final       Writer              traceWriter;
  25.   private final       Writer              debugWriter;
  26.   private final       Writer              infoWriter;
  27.   private final       int                 forcePrintLevelForUnexercisedActions;
  28.   String previousIndent = "";

  29.   /**
  30.    * Creates an object of this class.
  31.    * This reporter implements a functionality to squash actions to make output concise.
  32.    * It is controlled by a predicate given through `conditionToSquashAction` parameter.
  33.    * Also, depending on whether an action is exercised or not, a writer used to print it out will be chosen by internal logic of this class.
  34.    * Furthermore, actions in a deep level of a tree will be considered "unexercised" forcibly.
  35.    * The threshold level can be controlled by the parameter `forcePrintLevelForUnexercisedActions`.
  36.    *
  37.    * @param conditionToSquashAction              A condition to "squash" a record of an action to make report concise.
  38.    * @param warnWriter                           A writer used for "warn" level.
  39.    * @param infoWriter                           A writer used for "info" level.
  40.    * @param debugWriter                          A writer used for "debug" level.
  41.    * @param traceWriter                          A writer used for "trace" level.
  42.    * @param report                               A map that stores an action tree's result.
  43.    * @param forcePrintLevelForUnexercisedActions A level under which action should be considered to be "unexercised".
  44.    */
  45.   public ActionReporter(Predicate<Action> conditionToSquashAction, Writer warnWriter, Writer infoWriter, Writer debugWriter, Writer traceWriter, Map<Action, Record> report, int forcePrintLevelForUnexercisedActions) {
  46.     super(infoWriter);
  47.     this.conditionToSquashAction = conditionToSquashAction;
  48.     this.report = requireNonNull(report);
  49.     this.warnWriter = requireNonNull(warnWriter);
  50.     this.debugWriter = requireNonNull(debugWriter);
  51.     this.infoWriter = infoWriter;
  52.     this.traceWriter = traceWriter;
  53.     this.forcePrintLevelForUnexercisedActions = forcePrintLevelForUnexercisedActions;
  54.   }

  55.   /**
  56.    * Creates an action of this class.
  57.    *
  58.    * @param writer A writer through which a report is written.
  59.    * @param report A report data that records the result of actions in the tree.
  60.    */
  61.   public ActionReporter(Writer writer, Map<Action, Record> report) {
  62.     this(DEFAULT_CONDITION_TO_SQUASH_ACTION, writer, writer, writer, writer, report, 2);
  63.   }

  64.   /**
  65.    * An entry-point to start reporting.
  66.    *
  67.    * @param action An action from which reporting starts.
  68.    */
  69.   public void report(Action action) {
  70.     requireNonNull(action).accept(this);
  71.   }

  72.   @Override
  73.   protected void handleAction(Action action) {
  74.     if (this.conditionToSquashAction.test(action)) {
  75.       this.previousIndent = indent();
  76.       return;
  77.     }
  78.     Record runs = report.get(action);
  79.     String message = format("%s[%s]%s", indent(), runs != null ? runs : "", action);
  80.     this.previousIndent = "";
  81.     if (isInFailingContext()) {
  82.       this.warnWriter.writeLine(message);
  83.     } else {
  84.       if (emptyLevel < 1) { // Top level unexercised + exercised ones
  85.         if (passingLevels() < 1) {
  86.           this.infoWriter.writeLine(message);
  87.         } else {
  88.           writeLineForUnexercisedAction(message);
  89.         }
  90.       } else {
  91.         writeLineForUnexercisedAction(message);
  92.       }
  93.     }
  94.   }

  95.   /**
  96.    * //@formatter.off
  97.    * ----
  98.    * > [E:0]for each of (noname) parallely
  99.    * >  +-[EE:0]print1
  100.    * >    |   [EE:0](noname)
  101.    * >    +-[]print2
  102.    * >      |   [](noname)
  103.    * >      +-[]print2-1
  104.    * >      |   [](noname)
  105.    * >      +-[]print2-2
  106.    * >        [](noname)
  107.    * ----
  108.    * //@format:on
  109.    */
  110.   @Override
  111.   public String indent() {
  112.     List<? extends Action> path = this.path();
  113.     StringBuilder b = new StringBuilder();
  114.     if (!path.isEmpty()) {
  115.       Action last = path.get(path.size() - 1);
  116.       for (Action each : path) {
  117.         if (each instanceof Composite) {
  118.           if (each == last) {
  119.             if (((Composite) each).isParallel())
  120.               b.append("*-");
  121.             else
  122.               b.append("+-");
  123.           } else {
  124.             if (isLastChild(nextOf(each, path), each))
  125.               b.append("  ");
  126.             else
  127.               b.append("| ");
  128.           }
  129.         } else {
  130.           b.append("  ");
  131.         }
  132.       }
  133.     }
  134.     return mergeStrings(this.previousIndent, b.toString());
  135.   }

  136.   @Override
  137.   protected void enter(Action action) {
  138.     super.enter(action);
  139.     depth++;
  140.     Record runs = report.get(action);
  141.     pushFailingContext(runs != null && runs.allFailing());
  142.     if (runs == null)
  143.       emptyLevel++;

  144.   }

  145.   @Override
  146.   protected void leave(Action action) {
  147.     Record runs = report.get(action);
  148.     if (runs == null)
  149.       emptyLevel--;
  150.     popFailingContext();
  151.     depth--;
  152.     super.leave(action);
  153.   }

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

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

  160.   void popFailingContext() {
  161.     failingContext.remove(0);
  162.   }

  163.   private void writeLineForUnexercisedAction(String message) {
  164.     // unexercised
  165.     if (depth < this.forcePrintLevelForUnexercisedActions)
  166.       this.debugWriter.writeLine(message);
  167.     else
  168.       this.traceWriter.writeLine(message);
  169.   }

  170.   private int passingLevels() {
  171.     int ret = 0;
  172.     for (boolean each : this.failingContext)
  173.       if (!each)
  174.         ret++;
  175.     return ret;
  176.   }

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

  180.   /**
  181.    * Merges two string into one.
  182.    * A white space in `a` or `b` will be replaced with non-white space in the other at the same position.
  183.    * In case both have non-white space in the same position, the latter's (`b`) overrides the first's (`a`).
  184.    * <p>
  185.    * .Example input
  186.    * ----
  187.    * a:"hello    "
  188.    * b:"    O WORLD "
  189.    * ----
  190.    * <p>
  191.    * .Example output
  192.    * ----
  193.    * "hellO WORLD "
  194.    * ----
  195.    *
  196.    * @param a A string to be merged.
  197.    * @param b A stringto be merged
  198.    * @return The merged result string.
  199.    */
  200.   private static String mergeStrings(String a, String b) {
  201.     StringBuilder builder = new StringBuilder();
  202.     int min = Math.min(a.length(), b.length());
  203.     for (int i = 0; i < min; i++) {
  204.       char ach = a.charAt(i);
  205.       char bch = b.charAt(i);
  206.       if (bch != ' ')
  207.         builder.append(bch);
  208.       else
  209.         builder.append(ach);
  210.     }
  211.     // Whichever longer, the result is the same since the shorter.substring(min) will become an empty
  212.     // string.
  213.     builder.append(a.substring(min));
  214.     builder.append(b.substring(min));
  215.     return builder.toString();
  216.   }

  217.   private static boolean isLastChild(Action each, Action parent) {
  218.     if (parent instanceof Composite) {
  219.       int index = ((Composite) parent).children().indexOf(each);
  220.       int size = ((Composite) parent).children().size();
  221.       assert index >= 0;
  222.       return index == size - 1;
  223.     }
  224.     return true;
  225.   }
  226. }