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

com.trivago.features.FeatureFileConverter Maven / Gradle / Ivy

Go to download

Plugin for slicing Cucumber features into the smallest possible parts for parallel test execution.

There is a newer version: 1.12.0
Show newest version
/*
 * Copyright 2017 trivago N.V.
 *
 * 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.trivago.features;

import com.trivago.exceptions.CucablePluginException;
import com.trivago.exceptions.filesystem.FileCreationException;
import com.trivago.exceptions.filesystem.MissingFileException;
import com.trivago.files.FileIO;
import com.trivago.files.FileSystemManager;
import com.trivago.gherkin.GherkinDocumentParser;
import com.trivago.logging.CucableLogger;
import com.trivago.logging.Language;
import com.trivago.properties.PropertyManager;
import com.trivago.runners.RunnerFileContentRenderer;
import com.trivago.vo.CucableFeature;
import com.trivago.vo.FeatureRunner;
import com.trivago.vo.SingleScenario;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import static com.trivago.logging.CucableLogger.CucableLogLevel.COMPACT;
import static com.trivago.logging.CucableLogger.CucableLogLevel.DEFAULT;
import static com.trivago.logging.CucableLogger.CucableLogLevel.MINIMAL;

/**
 * This class is responsible for converting feature files
 * into single scenario feature files and runners.
 */
@Singleton
public class FeatureFileConverter {

    private static final String FEATURE_FILE_EXTENSION = ".feature";
    private static final String RUNNER_FILE_EXTENSION = ".java";
    private static final String INTEGRATION_TEST_POSTFIX = "_IT";
    private static final String PATH_SEPARATOR = "/";
    private static final String TEST_RUNS_COUNTER_FORMAT = "_run%03d";
    private static final String FEATURE_COUNTER_FORMAT = "_feature%03d";
    private static final String SCENARIO_COUNTER_FORMAT = "_scenario%03d";

    private final PropertyManager propertyManager;
    private final GherkinDocumentParser gherkinDocumentParser;
    private final FeatureFileContentRenderer featureFileContentRenderer;
    private final RunnerFileContentRenderer runnerFileContentRenderer;
    private final FileIO fileIO;
    private FileSystemManager fileSystemManager;
    private final CucableLogger logger;

    // Holds the current number of single features per feature key
    // (in a scenario outline, each example yields a single feature with the same key).
    private final Map singleFeatureCounters = new HashMap<>();

    @Inject
    public FeatureFileConverter(
            PropertyManager propertyManager,
            GherkinDocumentParser gherkinDocumentParser,
            FeatureFileContentRenderer featureFileContentRenderer,
            RunnerFileContentRenderer runnerFileContentRenderer,
            FileIO fileIO,
            FileSystemManager fileSystemManager,
            CucableLogger logger
    ) {
        this.propertyManager = propertyManager;
        this.gherkinDocumentParser = gherkinDocumentParser;
        this.featureFileContentRenderer = featureFileContentRenderer;
        this.runnerFileContentRenderer = runnerFileContentRenderer;
        this.fileIO = fileIO;
        this.fileSystemManager = fileSystemManager;
        this.logger = logger;
    }

    /**
     * Converts a list of Cucable features
     *
     * @param cucableFeatures feature files to process
     * @throws CucablePluginException see {@link CucablePluginException}
     */
    public void generateParallelizableFeatures(
            final List cucableFeatures) throws CucablePluginException {

        int featureFileCounter = 0;
        List allGeneratedFeaturePaths = new ArrayList<>();

        for (CucableFeature cucableFeature : cucableFeatures) {
            List paths = fileSystemManager.getPathsFromCucableFeature(cucableFeature);
            for (Path path : paths) {
                List generatedFeatureFilePaths = generateParallelizableFeatures(path, cucableFeature.getLineNumbers());
                allGeneratedFeaturePaths.addAll(generatedFeatureFilePaths);
                featureFileCounter += generatedFeatureFilePaths.size();
            }
        }

        int runnerFileCounter;
        if (propertyManager.getDesiredNumberOfFeaturesPerRunner() > 0) {
            runnerFileCounter = generateRunnerClassesWithDesiredNumberOfFeatures(
                    allGeneratedFeaturePaths, propertyManager.getDesiredNumberOfFeaturesPerRunner()
            );
        } else {
            runnerFileCounter = generateRunnerClassesWithDesiredNumberOfRunners(
                    allGeneratedFeaturePaths, propertyManager.getDesiredNumberOfRunners()
            );
        }

        logger.logInfoSeparator(DEFAULT);
        logger.info(
                String.format("Cucable created %d separate %s and %d %s.",
                        featureFileCounter,
                        Language.singularPlural(featureFileCounter, "feature file", "feature files"),
                        runnerFileCounter,
                        Language.singularPlural(runnerFileCounter, "runner", "runners")
                ),
                DEFAULT, COMPACT, MINIMAL
        );
    }

    /**
     * Converts all scenarios in the given feature file to single
     * scenario feature files and their respective runners.
     *
     * @param sourceFeatureFilePath feature file to process.
     * @param lineNumbers           scenario line numbers.
     * @return Number of created scenarios.
     * @throws CucablePluginException see {@link CucablePluginException}
     */
    private List generateParallelizableFeatures(
            final Path sourceFeatureFilePath,
            final List lineNumbers) throws CucablePluginException {

        if (propertyManager.getParallelizationMode() == PropertyManager.ParallelizationMode.FEATURES) {
            return generateFeaturesWithFeaturesParallelizationMode(sourceFeatureFilePath);
        }
        return generateFeaturesWithScenariosParallelizationMode(sourceFeatureFilePath, lineNumbers);
    }

    /**
     * Generate features with parallelization mode 'features'.
     *
     * @param sourceFeatureFilePath Path of the source feature(s).
     * @return A list of generated feature paths.
     * @throws CucablePluginException in case feature files cannot be created.
     */
    private List generateFeaturesWithFeaturesParallelizationMode(final Path sourceFeatureFilePath) throws
            CucablePluginException {
        String featureFilePathString = sourceFeatureFilePath.toString();
        String featureFileContent = fileIO.readContentFromFile(featureFilePathString);
        return generateFeatureFiles(sourceFeatureFilePath, featureFileContent);
    }

    /**
     * Generate features with parallelization mode 'scenarios'.
     *
     * @param sourceFeatureFilePath path of the source feature(s).
     * @param lineNumbers           scenario line numbers.
     * @return A list of generated feature paths.
     * @throws CucablePluginException in case feature files cannot be created.
     */
    private List generateFeaturesWithScenariosParallelizationMode(
            final Path sourceFeatureFilePath,
            final List lineNumbers) throws CucablePluginException {

        String featureFilePathString = sourceFeatureFilePath.toString();
        if (featureFilePathString == null || featureFilePathString.equals("")) {
            throw new MissingFileException(featureFilePathString);
        }

        String featureFileContent = fileIO.readContentFromFile(featureFilePathString);

        List singleScenarios;
        singleScenarios =
                gherkinDocumentParser.getSingleScenariosFromFeature(
                        featureFileContent,
                        featureFilePathString,
                        lineNumbers
                );
        return generateFeatureFiles(sourceFeatureFilePath, singleScenarios);
    }

    /**
     * Write a single scenario feature to new files and return a list of generated file paths for later runner creation.
     *
     * @param sourceFeatureFilePath The complete path to the source feature.
     * @param singleScenarios       a list of single scenarios.
     * @return A list of generated feature file paths.
     * @throws FileCreationException Thrown if the feature file cannot be created.
     */
    private List generateFeatureFiles(
            final Path sourceFeatureFilePath,
            final List singleScenarios) throws FileCreationException {

        // Stores all generated feature file names and associated source feature paths for later runner creation
        List generatedFeaturePaths = new ArrayList<>();

        // Default parallelization mode
        for (SingleScenario singleScenario : singleScenarios) {
            String featureFileName = getFeatureFileNameFromPath(sourceFeatureFilePath);
            Integer featureCounter = singleFeatureCounters.getOrDefault(featureFileName, 0);
            featureCounter++;
            String scenarioCounterFilenamePart = String.format(SCENARIO_COUNTER_FORMAT, featureCounter);
            for (int testRuns = 1; testRuns <= propertyManager.getNumberOfTestRuns(); testRuns++) {
                String testRunsCounterFilenamePart = String.format(TEST_RUNS_COUNTER_FORMAT, testRuns);
                String generatedFileName =
                        featureFileName
                                .concat(scenarioCounterFilenamePart)
                                .concat(testRunsCounterFilenamePart)
                                .concat(INTEGRATION_TEST_POSTFIX);
                saveFeature(
                        generatedFileName,
                        featureFileContentRenderer.getRenderedFeatureFileContent(singleScenario)
                );
                generatedFeaturePaths.add(generatedFileName);
                singleFeatureCounters.put(featureFileName, featureCounter);
            }
        }

        logFeatureFileConversionMessage(sourceFeatureFilePath.toString(), singleScenarios.size());
        return generatedFeaturePaths;
    }

    /**
     * Write a copy of the original feature file content to new files and return a list of generated file paths for later runner creation.
     *
     * @param sourceFeatureFilePath The complete path to the source feature.
     * @param featureFileContent    the content to copy into the new feature file.
     * @return A list of generated feature file paths.
     * @throws FileCreationException Thrown if the feature file cannot be created.
     */
    private List generateFeatureFiles(
            final Path sourceFeatureFilePath, final String featureFileContent) throws FileCreationException {

        // Stores all generated feature file names and associated source feature paths for later runner creation
        List generatedFeaturePaths = new ArrayList<>();

        // Only parallelize complete features
        String featureFileName = getFeatureFileNameFromPath(sourceFeatureFilePath);
        Integer featureCounter = singleFeatureCounters.getOrDefault(featureFileName, 0);
        featureCounter++;
        String featureCounterFilenamePart = String.format(FEATURE_COUNTER_FORMAT, featureCounter);
        for (int testRuns = 1; testRuns <= propertyManager.getNumberOfTestRuns(); testRuns++) {
            String testRunsCounterFilenamePart = String.format(TEST_RUNS_COUNTER_FORMAT, testRuns);
            String generatedFileName =
                    featureFileName
                            .concat(featureCounterFilenamePart)
                            .concat(testRunsCounterFilenamePart)
                            .concat(INTEGRATION_TEST_POSTFIX);
            saveFeature(
                    generatedFileName,
                    featureFileContent
            );
            generatedFeaturePaths.add(generatedFileName);
            singleFeatureCounters.put(featureFileName, featureCounter);
        }

        logger.info(String.format("- processed %s.", featureFileName), DEFAULT);

        return generatedFeaturePaths;
    }

    /**
     * Save generated feature file contents and return the file path.
     *
     * @param featureFileName            The feature file name.
     * @param renderedFeatureFileContent The rendered file contents.
     * @throws FileCreationException if the file cannot be created.
     */
    private void saveFeature(final String featureFileName, final String renderedFeatureFileContent)
            throws FileCreationException {

        String generatedFeatureFilePath =
                propertyManager.getGeneratedFeatureDirectory()
                        .concat(PATH_SEPARATOR)
                        .concat(featureFileName)
                        .concat(FEATURE_FILE_EXTENSION);

        // Save scenario information to new feature file
        fileIO.writeContentToFile(renderedFeatureFileContent, generatedFeatureFilePath);
    }

    /**
     * Generate runner classes for a list of feature file paths.
     *
     * @param generatedFeatureNames  The list of generated feature file names.
     * @param numberOfDesiredRunners The number of desired runners (if set to 0, a runner is generated for each feature file path).
     * @return The number of generated runners.
     * @throws CucablePluginException see {@link CucablePluginException}.
     */
    private int generateRunnerClassesWithDesiredNumberOfRunners(final List generatedFeatureNames, final int numberOfDesiredRunners) throws
            CucablePluginException {

        int targetRunnerNumber = numberOfDesiredRunners;
        if (targetRunnerNumber == 0) {
            targetRunnerNumber = generatedFeatureNames.size();
        }

        List> generatedFeatureNamesPerRunner = new ArrayList<>(targetRunnerNumber);
        for (int i = 0; i < targetRunnerNumber; i++) {
            generatedFeatureNamesPerRunner.add(new ArrayList<>());
        }

        int currentRunnerIndex = 0;
        for (String generatedFeatureName : generatedFeatureNames) {
            generatedFeatureNamesPerRunner.get(currentRunnerIndex).add(generatedFeatureName);
            currentRunnerIndex++;
            if (currentRunnerIndex >= targetRunnerNumber) {
                currentRunnerIndex = 0;
            }
        }

        int runnerFileCounter = 0;
        for (List generatedFeatureNamesForSingleRunner : generatedFeatureNamesPerRunner) {
            if (generatedFeatureNamesForSingleRunner.size() > 0) {
                generateRunnerClass(generatedFeatureNamesForSingleRunner);
                runnerFileCounter++;
            }
        }

        return runnerFileCounter;
    }

    /**
     * Generate runner classes for a list of feature file paths.
     *
     * @param generatedFeatureNames            The list of generated feature file names.
     * @param numberOfDesiredFeaturesPerRunner The number of desired features per runner.
     * @return The number of generated runners.
     * @throws CucablePluginException see {@link CucablePluginException}.
     */
    private int generateRunnerClassesWithDesiredNumberOfFeatures(
            final List generatedFeatureNames,
            final int numberOfDesiredFeaturesPerRunner)
            throws CucablePluginException {

        int currentRunnerFeatureCount = 0;
        int totalFeatureCount = 0;
        List generatedFeatureNamesForSingleRunner = new ArrayList<>();
        int runnerFileCounter = 0;
        for (String generatedFeatureName : generatedFeatureNames) {
            generatedFeatureNamesForSingleRunner.add(generatedFeatureName);
            currentRunnerFeatureCount++;
            totalFeatureCount++;
            if (totalFeatureCount == generatedFeatureNames.size() ||
                    currentRunnerFeatureCount >= numberOfDesiredFeaturesPerRunner) {
                runnerFileCounter++;
                generateRunnerClass(generatedFeatureNamesForSingleRunner);
                currentRunnerFeatureCount = 0;
                generatedFeatureNamesForSingleRunner = new ArrayList<>();
            }
        }

        return runnerFileCounter;
    }


    /**
     * Generate a single runner class from a list of feature files.
     *
     * @param generatedFeatureFileNames The list of generated generated feature file names.
     * @throws CucablePluginException see {@link CucablePluginException}.
     */
    private void generateRunnerClass(final List generatedFeatureFileNames) throws CucablePluginException {

        // The runner class name will be equal to the feature name if there is only one feature to run.
        // Otherwise, a generated runner class name is used.
        String runnerClassName;
        if (generatedFeatureFileNames.size() == 1) {
            runnerClassName = generatedFeatureFileNames.get(0);
        } else {
            runnerClassName = "CucableMultiRunner_"
                    .concat(UUID.randomUUID().toString().replace("-", "_"))
                    .concat(INTEGRATION_TEST_POSTFIX);
        }

        // Generate runner for the newly generated single scenario feature file
        FeatureRunner featureRunner =
                new FeatureRunner(
                        propertyManager.getSourceRunnerTemplateFile(), runnerClassName, generatedFeatureFileNames);

        String renderedRunnerClassContent =
                runnerFileContentRenderer.getRenderedRunnerFileContent(featureRunner);

        String generatedRunnerClassFilePath =
                propertyManager.getGeneratedRunnerDirectory()
                        .concat(PATH_SEPARATOR)
                        .concat(runnerClassName)
                        .concat(RUNNER_FILE_EXTENSION);

        fileIO.writeContentToFile(renderedRunnerClassContent, generatedRunnerClassFilePath);
    }

    /**
     * Log the completion message for a feature file.
     *
     * @param featureFileName  The name of the processed feature file.
     * @param createdScenarios The number of created scenarios for the feature file.
     */
    private void logFeatureFileConversionMessage(
            final String featureFileName,
            final int createdScenarios) {

        String logPostfix = ".";
        logger.info(
                String.format("- generated %3d %s %s%s",
                        createdScenarios,
                        Language.singularPlural(createdScenarios, " scenario from", "scenarios from"),
                        featureFileName,
                        logPostfix
                ), DEFAULT);
    }

    /**
     * Get the feature file name without the extension and with replaced special chars from the full feature file path.
     *
     * @param featureFilePath The path to the feature file.
     * @return The cleaned up feature file name.
     */
    private String getFeatureFileNameFromPath(final Path featureFilePath) {
        String fullFeatureFileName = featureFilePath.getFileName().toString();
        String featureFileName = fullFeatureFileName.substring(0, fullFeatureFileName.lastIndexOf("."));
        return featureFileName.replaceAll("\\W", "_");
    }
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy