001package org.hl7.fhir.utilities.tests.execution.junit5;
002
003import lombok.Getter;
004import lombok.Setter;
005import org.hl7.fhir.utilities.tests.execution.CliTestSummary;
006import org.hl7.fhir.utilities.tests.execution.ModuleTestExecutor;
007import org.junit.platform.engine.TestExecutionResult;
008import org.junit.platform.launcher.*;
009import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
010import org.junit.platform.launcher.core.LauncherFactory;
011import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
012import org.junit.platform.launcher.listeners.TestExecutionSummary;
013
014import java.io.PrintStream;
015import java.util.Arrays;
016import java.util.Collections;
017import java.util.List;
018import java.util.stream.Collectors;
019
020import static org.junit.platform.engine.discovery.ClassNameFilter.includeClassNamePatterns;
021import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage;
022
023/**
024 * This class allows JUnit 5 tests to be run in Java runtime.
025 *
026 * JUnit 4 tests ARE NOT SUPPORTED.
027 *
028 * This mimics for the most part the output of the Maven Surefire plugin, with
029 * additional summaries and unique IDs for diagnosing failing tests.
030 */
031public class JUnit5ModuleTestExecutor extends ModuleTestExecutor {
032
033
034
035  private static final String SUMMARY_TEMPLATE = "Tests run: %d, Failures: %d, Errors: %d, Skipped: %d";
036
037  private static final String MODULE_FINISHED_TEMPLATE = "%s module tests finished.";
038  public static final String DEFAULT_CLASSNAME_FILTER = org.junit.platform.engine.discovery.ClassNameFilter.STANDARD_INCLUDE_PATTERN;
039
040
041  @Getter
042  private final String moduleName;
043
044  @Getter
045  private final List<String> packageNames;
046
047  public JUnit5ModuleTestExecutor(String moduleName, List<String> packageNames) {
048    this.moduleName = moduleName;
049    this.packageNames = Collections.unmodifiableList(packageNames);
050  }
051
052  private List<org.junit.platform.engine.discovery.PackageSelector> getPackageSelectors() {
053    final List<String> packageNames = getPackageNames();
054    return packageNames.stream().map(it -> selectPackage(it)).collect(Collectors.toList());
055  }
056
057  class ModuleTestExecutionListener extends SummaryGeneratingListener {
058
059    @Setter
060    private PrintStream out;
061
062    @Override
063    public void executionStarted(TestIdentifier testIdentifier) {
064      ModuleTestExecutor.printTestStarted(out, testIdentifier.getDisplayName());
065      super.executionStarted(testIdentifier);
066    }
067
068    @Override
069    public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
070      //
071      if (testExecutionResult.getStatus().equals(TestExecutionResult.Status.FAILED)) {
072        ModuleTestExecutor.printTestFailed(out, testIdentifier.getUniqueId(),  testExecutionResult.getThrowable().isPresent()
073          ? testExecutionResult.getThrowable().get()
074          :null);
075      }
076      ModuleTestExecutor.printTestFinished(out,
077        testIdentifier.getDisplayName(),
078        testExecutionResult.getStatus().name()
079      );
080      super.executionFinished(testIdentifier, testExecutionResult);
081    }
082  }
083
084  /**
085   * Execute all tests defined in this module and return a TestExecutionSummary.
086   *
087   * Any stack traces required to diagnose test failures will be output at this
088   * stage, along with a unique test ID.
089   *
090   * @param out the PrintStream to log execution or test failure.
091   * @param classNameFilter a Regex to use to select tests. If null, this will
092   *                        default to the JUnit console filter.
093   * @return The test execution summary.
094   *
095   * @see org.junit.platform.engine.discovery.ClassNameFilter
096   */
097  public CliTestSummary executeTests(PrintStream out, String classNameFilter) {
098    LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
099      .selectors(
100        getPackageSelectors()
101      )
102      .filters(
103        includeClassNamePatterns(classNameFilter == null ? DEFAULT_CLASSNAME_FILTER : classNameFilter)
104      )
105      .build();
106
107    ModuleTestExecutionListener moduleTestExecutionlistener = new ModuleTestExecutionListener();
108    moduleTestExecutionlistener.setOut(out);
109
110    try (LauncherSession session = LauncherFactory.openSession()) {
111      Launcher launcher = session.getLauncher();
112
113      launcher.registerTestExecutionListeners(moduleTestExecutionlistener);
114
115      TestPlan testPlan = launcher.discover(request);
116      // Execute test plan
117      launcher.execute(testPlan);
118      // Alternatively, execute the request directly
119      // launcher.execute(request);
120    }
121
122    TestExecutionSummary summary = moduleTestExecutionlistener.getSummary();
123
124    out.println("\n" + String.format(MODULE_FINISHED_TEMPLATE, getModuleName()));
125    out.println(String.format(SUMMARY_TEMPLATE, summary.getTestsFoundCount(), summary.getTestsFailedCount(), summary.getTestsAbortedCount(), summary.getTestsSkippedCount()));
126
127    return new JUnit5TestSummaryAdapter(summary);
128  }
129
130
131
132  /**
133   * Create a ModuleTestExecutor for the standard case where the module name
134   * maps to the package name of the root package containing tests.
135   *
136   * For example the module "org.hl7.fhir.r5" will be assumed to have all its
137   * tests in the "org.hl7.fhir.r5" package or its child packages.
138   *
139   * @param moduleName The name of the module and root package containing tests.
140   * @return the ModuleTestExecutor for the module
141   */
142  public static JUnit5ModuleTestExecutor getStandardModuleTestExecutor(String moduleName) {
143    return  new JUnit5ModuleTestExecutor(moduleName, Arrays.asList(moduleName));
144  }
145}