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

com.github.sbt.junit.jupiter.internal.Configuration Maven / Gradle / Ivy

The newest version!
/*
 * jupiter-interface
 *
 * Copyright (c) 2017, Michael Aichler.
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.sbt.junit.jupiter.internal;

import com.github.sbt.junit.jupiter.api.JupiterTestListener;
import com.github.sbt.junit.jupiter.internal.listeners.FlatPrintingTestListener;
import com.github.sbt.junit.jupiter.internal.listeners.TreePrintingTestListener;
import com.github.sbt.junit.jupiter.internal.options.Options;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.UniqueId.Segment;
import org.junit.platform.engine.support.descriptor.ClassSource;
import org.junit.platform.engine.support.descriptor.MethodSource;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
import sbt.testing.Logger;
import sbt.testing.TaskDef;

/** @author Michael Aichler */
@SuppressWarnings("WeakerAccess")
public class Configuration {

  private final Options options;
  private final TestLogger logger;
  private final ColorTheme colorTheme = new ColorTheme() {};
  private final String testSuiteName;

  public Configuration(String testSuiteName, Logger[] loggers, Options options) {

    this.options = options;
    this.testSuiteName = testSuiteName;
    this.logger = new TestLogger(loggers, this);
  }

  /** @return The configured color theme. */
  public ColorTheme getColorTheme() {

    return colorTheme;
  }

  /** @return The configured test logger. */
  public TestLogger getLogger() {

    return logger;
  }

  /** @return The provided commandline options. */
  public Options getOptions() {

    return options;
  }

  /**
   * @see TaskDef#fullyQualifiedName()
   * @return The name of the test suite which is currently executed.
   */
  public String getTestSuiteName() {

    return testSuiteName;
  }

  /** @return The configured test listener. */
  public JupiterTestListener getTestListener() {

    switch (options.getDisplayMode()) {
      case "tree":
        return new TreePrintingTestListener(this);
      default:
        return new FlatPrintingTestListener(this);
    }
  }

  /**
   * Creates instances of test listeners using the specified {@code classLoader}.
   *
   * @param classLoader The class loader which should be used to load test listeners.
   * @return The list of test listener instances supplied via the command line.
   */
  public Optional createRunListener(ClassLoader classLoader) {

    return options
        .getRunListener()
        .map(
            className -> {
              try {
                final Object listener = classLoader.loadClass(className).newInstance();
                return (TestExecutionListener) listener;
              } catch (Exception e) {
                throw new RuntimeException(e);
              }
            });
  }

  /**
   * @param name The name which is to be decoded.
   * @return The decoded name, if {@link Options#isDecodeScalaNames()} evaluates to true.
   */
  public String decodeName(String name) {

    if (!options.isDecodeScalaNames()) {
      return name;
    }

    try {
      Class cl = Class.forName("scala.reflect.NameTransformer");
      Method m = cl.getMethod("decode", String.class);
      String decoded = (String) m.invoke(null, name);
      return decoded == null ? name : decoded;
    } catch (Throwable t) {
      return name;
    }
  }

  /**
   * @param testPlan The test plan of the specified identifier.
   * @param identifier The test identifier which is to be formatted.
   * @return The formatted display name for the specified identifier.
   */
  public String formatIdentifier(TestPlan testPlan, TestIdentifier identifier) {

    return new TestIdentifierFormatter(testPlan, identifier).format();
  }

  /**
   * Extracts the class-name or alternatively the display name from the given identifier.
   *
   * @param identifier The identifier from which to extract the class-name or display name.
   * @return The class-name of the specified test identifier if a source is attached to the
   *     identifier, otherwise the display name is returned.
   */
  public String extractClassNameOrDisplayName(TestIdentifier identifier) {
    return identifier
        .getSource()
        .map(s -> extractClassName(identifier))
        .orElse(identifier.getDisplayName());
  }

  /**
   * Extracts the class-name from the specified test identifier.
   *
   * @param identifier The identifier from which to extract the class-name.
   * @return The class-name of the specified test identifier.
   */
  public String extractClassName(TestIdentifier identifier) {

    TestSource testSource =
        identifier
            .getSource()
            .orElseThrow(
                () -> new RuntimeException("Test identifier without source: " + identifier));

    if (testSource instanceof ClassSource) {
      return ((ClassSource) testSource).getClassName();
    }

    if (testSource instanceof MethodSource) {
      return ((MethodSource) testSource).getClassName();
    }

    throw new RuntimeException("Test identifier with unknown source: " + identifier);
  }

  /**
   * Extracts the method-name from the specified test identifier.
   *
   * @param identifier The identifier from which to extract the method-name.
   * @return The method-name of the specified test identifier.
   */
  public Optional extractMethodName(TestIdentifier identifier) {

    TestSource testSource = identifier.getSource().orElse(null);

    if (testSource instanceof MethodSource) {

      MethodSource methodSource = (MethodSource) testSource;
      return Optional.of(methodSource.getMethodName());
    }

    return Optional.empty();
  }

  public String buildInfoMessage(Throwable t) {
    return buildColoredMessage(t, colorTheme.normalName2());
  }

  public String buildInfoName(TestIdentifier identifier) {
    return buildColoredName(
        identifier, colorTheme.normalName1(), colorTheme.normalName2(), colorTheme.normalName3());
  }

  public String buildErrorMessage(Throwable t) {

    return buildColoredMessage(t, colorTheme.errorName2());
  }

  public String buildErrorName(TestIdentifier identifier) {
    return buildColoredName(
        identifier, colorTheme.errorName1(), colorTheme.errorName2(), colorTheme.errorName3());
  }

  private String buildColoredName(TestIdentifier identifier, Color c1, Color c2, Color c3) {

    String className = extractClassNameOrDisplayName(identifier);
    Optional methodName = extractMethodName(identifier);

    StringBuilder b = new StringBuilder();
    b.append(buildColoredClassName(decodeName(className), c1));

    methodName.ifPresent(m -> b.append(buildColoredMethodName(m, c2, c3)));
    return b.toString();
  }

  private String buildColoredMessage(Throwable t, Color color) {

    if (t == null) {
      return "null";
    }

    if (!options.isExceptionClassLogEnabled()) {
      return t.getMessage();
    }

    if (!options.isAssertLogEnabled()) {
      if (t instanceof AssertionError) {
        return t.getMessage();
      }
    }

    String className = decodeName(t.getClass().getName());
    return buildColoredClassName(className, color) + ": " + t.getMessage();
  }

  private String buildColoredClassName(String className, Color color) {

    int nestedClassPos = className.indexOf('$');
    int simpleNamePos =
        nestedClassPos == -1
            ? className.lastIndexOf('.')
            : className.lastIndexOf('.', nestedClassPos);

    if (simpleNamePos == -1) {
      return color.format(className);
    }

    String packagePrefix = className.substring(0, simpleNamePos);
    String simpleName = className.substring(simpleNamePos + 1);

    return packagePrefix + '.' + color.format(simpleName);
  }

  private String buildColoredMethodName(String m, Color c1, Color c2) {

    StringBuilder b = new StringBuilder();
    b.append('.');
    int mpos1 = m.lastIndexOf('[');
    int mpos2 = m.lastIndexOf(']');
    if (mpos1 == -1 || mpos2 < mpos1) {

      b.append(c1.format(decodeName(m)));
    } else {
      b.append(c1.format(decodeName(m.substring(0, mpos1))));
      b.append('[');
      b.append(c2.format(m.substring(mpos1 + 1, mpos2)));
      b.append(']');
    }

    return b.toString();
  }

  /** Helper class which knows how to format a {@link TestIdentifier}. */
  class TestIdentifierFormatter {

    static final String VINTAGE_ENGINE = "junit-vintage";

    final TestPlan testPlan;
    final TestIdentifier identifier;

    private String testEngine;

    TestIdentifierFormatter(TestPlan testPlan, TestIdentifier testIdentifier) {

      this.testPlan = testPlan;
      this.identifier = testIdentifier;
    }

    /** @return The formatted test name using the configured color theme. */
    public String format() {

      final List path = getPath(testPlan, identifier);

      // When run as part of a suite, the suite engine is the first segment, so look further
      testEngine =
          UniqueId.parse(identifier.getUniqueId()).getSegments().stream()
              .filter(segment -> segment.getType().equals("engine"))
              .map(Segment::getValue)
              .reduce((first, last) -> last)
              .orElse(null);

      return path.stream()
          .skip(1)
          .map(this::toName)
          .filter(Objects::nonNull)
          .collect(Collectors.joining());
    }

    private List getPath(TestPlan testPlan, TestIdentifier identifier) {

      List result = new ArrayList<>();

      do {
        if (identifier.getSource().isPresent()) {
          result.add(identifier);
        }
        identifier = testPlan.getParent(identifier).orElse(null);
      } while (null != identifier);

      Collections.reverse(result);
      return result;
    }

    /**
     * @return A formatted display name on success, {@code NULL} if the given identifier should be
     *     ignored.
     */
    private String toName(TestIdentifier identifier) {

      String name = identifier.getDisplayName();
      List segments = UniqueId.parse(identifier.getUniqueId()).getSegments();

      if (!segments.isEmpty()) {
        Segment lastSegment = segments.get(segments.size() - 1);

        name =
            VINTAGE_ENGINE.equals(testEngine)
                ? toVintageName(identifier, lastSegment)
                : toName(identifier, lastSegment);
      }

      return name;
    }

    /*
     * Formats a test segment from junit-jupiter.
     */
    private String toName(TestIdentifier identifier, Segment segment) {

      String name;

      switch (segment.getType()) {
        case "class":
          name = colorClassName(segment.getValue(), colorTheme.container());
          break;
        case "nested-class":
          name = colorTheme.container().format("$" + segment.getValue());
          break;
        case "method":
          name = colorTheme.testMethod().format("#" + segment.getValue());
          break;
        case "test-factory":
          name = colorTheme.testFactory().format("#" + segment.getValue());
          break;
        case "dynamic-test":
          name = colorTheme.dynamicTest().format(":" + identifier.getDisplayName());
          break;
        case "test-template":
          name = colorTheme.testTemplate().format("#" + segment.getValue());
          break;
        case "test-template-invocation":
          name = colorTheme.container().format(":" + segment.getValue());
          break;
        default:
          name = segment.getValue();
          break;
      }

      if (options.isTypesEnabled()) {
        name = segment.getType() + ":" + name;
      }

      return name;
    }

    /*
     * Formats a test identifier run by junit-vintage engine.
     */
    private String toVintageName(TestIdentifier identifier, Segment lastSegment) {

      final String type = lastSegment.getType();

      if ("runner".equals(type)) {

        String className = identifier.getLegacyReportingName();
        return colorClassName(className, colorTheme.container());
      }

      if ("test".equals(type)) {

        final TestSource source = identifier.getSource().orElse(null);

        if (null == source) {
          // Caused by Parameterized test runner, display name usually is the index name in
          // brackets.
          // Ignored since the index name is repeated in the display name of the test method.
          return null;
        }

        if (source instanceof ClassSource) {
          String nestedClassName = "$" + identifier.getDisplayName().replaceFirst(".*?\\$", "");
          return colorTheme.container().format(nestedClassName);
        }

        if (source instanceof MethodSource) {
          String testName = "#" + identifier.getDisplayName();
          return colorTheme.testMethod().format(testName);
        }
      }

      return "/" + identifier.getDisplayName();
    }

    /*
     * Colors the last part of  with the given .
     */
    private String colorClassName(String className, Color color) {

      String[] parts = className.split("\\.");
      return IntStream.range(0, parts.length)
          .mapToObj(
              i -> {
                if (i == (parts.length - 1)) return color.format(parts[i]);
                return parts[i];
              })
          .collect(Collectors.joining("."));
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy