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

de.gematik.test.tiger.testenvmgr.env.ScenarioRunner Maven / Gradle / Ivy

/*
 * Copyright 2024 gematik GmbH
 *
 * 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 de.gematik.test.tiger.testenvmgr.env;

import static io.cucumber.core.options.Constants.EXECUTION_DRY_RUN_PROPERTY_NAME;
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;

import de.gematik.test.tiger.common.config.TigerConfigurationKeys;
import de.gematik.test.tiger.common.config.TigerGlobalConfiguration;
import de.gematik.test.tiger.testenvmgr.api.model.TestExecutionRequestDto;
import de.gematik.test.tiger.testenvmgr.util.ScenarioCollector;
import io.cucumber.messages.types.Location;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.junit.platform.engine.DiscoverySelector;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.TestTag;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.engine.discovery.UniqueIdSelector;
import org.junit.platform.engine.support.descriptor.ClasspathResourceSource;
import org.junit.platform.engine.support.descriptor.FilePosition;
import org.junit.platform.engine.support.descriptor.FileSource;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class ScenarioRunner {

  private static final LinkedHashSet scenarios =
      new LinkedHashSet<>(); // NOSONAR - important to keep order

  // Must be single threaded because we do not want to have multiple tests runs at the same time.
  // Since the Launcher.execute() runs synchronously, we have the guarantee that the runner waits
  // for the test to finish.
  private static final ExecutorService scenarioExecutionService =
      Executors.newSingleThreadExecutor();

  private final Map testExecutionResults = new ConcurrentHashMap<>();

  public static void addScenarios(Collection newScenarios) {
    scenarios.addAll(newScenarios);
  }

  public static void addScenarios(TestPlan testPlan) {
    addScenarios(ScenarioCollector.collectScenarios(testPlan));
  }

  public TestExecutionStatus enqueueExecutionSelectedTests(
      TestExecutionRequestDto testExecutionRequest) {

    var sources = testExecutionRequest.getSourceFiles();
    var tags = testExecutionRequest.getTags();
    var uniqueIds = testExecutionRequest.getTestUniqueIds();

    // if one of the sourceFiles / tags / uniqueIds lists is empty, their filter is not used. That
    // means all scenarios are selected for the next filter round.
    List selectors =
        scenarios.stream()
            .filter(testIdentifier -> sources.isEmpty() || anyFileMatches(sources, testIdentifier))
            .filter(testIdentifier -> tags.isEmpty() || anyTagMatches(tags, testIdentifier))
            .filter(
                testIdentifier ->
                    uniqueIds.isEmpty() || anyUniqueIdMatches(uniqueIds, testIdentifier))
            .map(t -> DiscoverySelectors.selectUniqueId(t.getUniqueId()))
            .toList();
    return runTests(selectors);
  }

  public TestExecutionStatus enqueueExecutionAllTests() {
    List selectors =
        scenarios.stream().map(t -> DiscoverySelectors.selectUniqueId(t.getUniqueId())).toList();
    return runTests(selectors);
  }

  public static void clearScenarios() {
    scenarios.clear();
  }

  private static boolean areUrisSameFile(URI uri1, URI uri2) {
    return uri1.normalize().getPath().equals(uri2.normalize().getPath());
  }

  public TestExecutionStatus runTest(ScenarioIdentifier scenarioIdentifier) {
    UniqueIdSelector selector = DiscoverySelectors.selectUniqueId(scenarioIdentifier.uniqueId());
    return runTests(List.of(selector));
  }

  protected TestExecutionStatus runTests(List selectors) {
    var initialConfiguration =
        TigerGlobalConfiguration.readMap(
            TigerConfigurationKeys.CUCUMBER_ENGINE_RUNTIME_CONFIGURATION.downsampleKey());
    initialConfiguration.put(EXECUTION_DRY_RUN_PROPERTY_NAME, "false");
    val uuidForThisRun = UUID.randomUUID();
    LauncherDiscoveryRequest runRequest =
        request().selectors(selectors).configurationParameters(initialConfiguration).build();

    Launcher launcher = LauncherFactory.create();
    TestPlan testPlan = launcher.discover(runRequest);

    val summaryListener = new TigerSummaryListener(testPlan);
    val testExecutionStatus = new TestExecutionStatus(uuidForThisRun, testPlan, summaryListener);
    testExecutionResults.put(uuidForThisRun, testExecutionStatus);
    scenarioExecutionService.execute(() -> launcher.execute(testPlan, summaryListener));
    return testExecutionStatus;
  }

  public static UniqueId findScenarioUniqueId(URI featurePath, Location location) {
    ScenarioLocation scenarioLocation = new ScenarioLocation(featurePath, location);
    return scenarios.stream()
        .filter(scenarioLocation::matches)
        .findAny()
        .orElseThrow(
            () -> new NoSuchElementException("No scenario found matching " + scenarioLocation))
        .getUniqueIdObject();
  }

  private boolean sameFile(TestIdentifier test, String filePath) {
    return test.getSource()
        .map(
            source -> {
              if (source instanceof FileSource fileSource) {
                return fileSource.getUri().normalize().toString().equals(filePath);
              } else if (source instanceof ClasspathResourceSource classpathResourceSource) {
                return ("classpath:" + classpathResourceSource.getClasspathResourceName())
                    .equals(filePath);
              } else {
                return false;
              }
            })
        .orElse(false);
  }

  private boolean anyFileMatches(List files, TestIdentifier test) {
    return files.stream().anyMatch(f -> sameFile(test, f));
  }

  private boolean anyTagMatches(List tags, TestIdentifier test) {
    return tags.stream()
        .anyMatch(t -> test.getTags().stream().map(TestTag::getName).toList().contains(t));
  }

  private boolean anyUniqueIdMatches(List uniqueIds, TestIdentifier test) {
    return uniqueIds.stream().anyMatch(u -> test.getUniqueId().equals(u));
  }

  public Optional getTestResults(UUID testRunId) {
    return Optional.ofNullable(testExecutionResults.get(testRunId));
  }

  public record ScenarioIdentifier(String uniqueId) {}

  public record ScenarioLocation(URI featurePath, Location testVariantLocation) {
    public FilePosition filePosition() {
      return FilePosition.from(
          testVariantLocation.getLine().intValue(),
          testVariantLocation
              .getColumn()
              .orElseThrow(() -> new RuntimeException("No column available for " + this))
              .intValue());
    }

    public boolean matches(TestIdentifier testIdentifier) {
      TestSource testSource =
          testIdentifier
              .getSource()
              .orElseThrow(
                  () ->
                      new IllegalArgumentException(
                          "No Test source available for " + testIdentifier));
      if (testSource instanceof FileSource fileSource) {
        return areUrisSameFile(fileSource.getUri(), featurePath())
            && fileSource
                .getPosition()
                .orElseThrow(
                    () -> new IllegalArgumentException("No position available for " + fileSource))
                .equals(filePosition());
      } else if (testSource instanceof ClasspathResourceSource classpathSource) {
        return ("classpath:" + classpathSource.getClasspathResourceName())
                .equals(featurePath().toString())
            && classpathSource
                .getPosition()
                .orElseThrow(
                    () ->
                        new IllegalArgumentException(
                            "No position available for " + classpathSource))
                .equals(filePosition());
      } else {
        return false;
      }
    }
  }

  public static Set getScenarios() {
    return Collections.unmodifiableSet(scenarios);
  }

  public record TestExecutionStatus(
      UUID testRunId, TestPlan testPlan, TigerSummaryListener testSummaryListener) {
    public TestExecutionSummary getSummary() {
      return testSummaryListener.getSummary();
    }

    public List getIndividualTestResults() {
      return testSummaryListener.getIndividualTestResults();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy