Skip to content

Commit

Permalink
implemented multi-threaded or parallel execution
Browse files Browse the repository at this point in the history
  • Loading branch information
ptrthomas committed Mar 19, 2017
1 parent 04f0921 commit 51dce38
Show file tree
Hide file tree
Showing 13 changed files with 716 additions and 131 deletions.
46 changes: 42 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ And you don't need to create Java objects (or POJO-s) for any of the payloads th
* Re-use of payload-data and user-defined functions across tests is so easy - that it becomes a natural habit for the test-developer
* Built-in support for switching configuration across different environments (e.g. dev, QA, pre-prod)
* Support for data-driven tests and being able to tag (or group) tests is built-in, no need to rely on TestNG or JUnit
* Seamless integration into existing Java projects as both JUnit and TestNG are supported
* Seamless integration into existing Java projects and CI / CD pipelines as both JUnit and TestNG are supported
* Support for multi-threaded parallel execution, which is a huge time-saver, especially for HTTP integration tests
* Easily invoke JDK classes, Java libraries, or re-use custom Java code if needed, for ultimate extensibility
* Simple plug-in system for authentication and HTTP header management that will handle any complex real-world scenario
* Comprehensive support for different flavors of HTTP calls:
Expand Down Expand Up @@ -315,11 +316,48 @@ You can 'lock down' the fact that you only want to execute the single JUnit clas
</plugin>
```

This is actually the recommended configuration for generating CI-friendly reports when using Cucumber. The `<disableXmlReport>` suppresses the default JUnit XML output normally emitted by the `maven-surefire-plugin`. And note how the `cucumber.options` can be specified using the `<systemProperties>` configuration. Options here would over-ride corresponding options specified if a `@CucumberOptions` annotation is present (on `AnimalsTest.java`). So for the above example, any `plugin` options present on the annotation would not take effect, but anything else (for example `tags`) would work.
This is actually the recommended configuration for generating CI-friendly reports when using Cucumber. The `<disableXmlReport>` suppresses the default JUnit XML output normally emitted by the `maven-surefire-plugin`. And note how the `cucumber.options` can be specified using the `<systemProperties>` configuration. Options here would over-ride corresponding options specified if a `@CucumberOptions` annotation is present (on `AnimalsTest.java`). So for the above example, any `plugin` options present on the annotation would not take effect, but anything else (for example `tags`) would continue to work.

With the above in place, you don't have to use `-Dtest=AnimalsTest` on the command-line any more. And the Cucumber JUnit XML reports would appear in the default `target/surefire-reports` directory (file names don't matter), and this will ensure that your CI and reporting routines work as you would expect. For example, the report would be in terms of how many Cucumber scenarios passed or failed.
With the above in place, you don't have to use `-Dtest=AnimalsTest` on the command-line any more. And the Cucumber JUnit XML report would appear within the default `target/surefire-reports` directory (file names don't matter), and this will ensure that your CI and reporting routines work as you would expect. For example, the report would be in terms of how many Cucumber scenarios passed or failed.

The [Karate Demo](karate-demo) has a working example of this set-up. Also refer to the section on [switching the environment](#switching-the-environment) for more ways of running tests via Maven using the command-line.
The [Karate Demo](karate-demo) has a working example of this set-up. Also refer to the section on [switching the environment](#switching-the-environment) for more ways of running tests via Maven using the command-line.

## Parallel Execution
Karate has experimental support for running tests in parallel, and here is how you make use of this capability:
```java
import com.intuit.karate.cucumber.CucumberRunner;
import com.intuit.karate.cucumber.KarateStats;
import cucumber.api.CucumberOptions;
import static org.junit.Assert.assertTrue;
import org.junit.Test;

@CucumberOptions(tags = {"~@ignore"})
public class AllParallel {

@Test
public void testParallel() {
KarateStats stats = CucumberRunner.parallel(getClass(), 5);
assertTrue("no scenario failed", stats.getFailCount() == 0);
}

}
```
Things to note:
* You don't use a JUnit runner, and you write a plain vanilla JUnit test using the `CucumberRunner` in `karate-core`.
* You can use the returned `KarateStats` to check if any scenarios failed.
* JUnit XML reports will be generated in `target/surefire-reports/` and CI tools should pick them up automatically.
* When using Maven, you must disable the JUnit default XML that normally gets generated using `<disableXmlReport>` (refer to the previous section on [test reports](#test-reports)).
* No other reports will be generated. If you specify a `plugin` option via the `@CucumberOptions` annotation (or the command-line) it will be ignored.
* For convenience, some stats are logged to the console when execution completes, which should look something like this:
```
======================================================
elapsed time: 1.778000 | test time: 7.895000
thread count: 5 | parallel efficiency: 0.888076
tests: 12 | failed: 0 | skipped: 0
======================================================
```

The [Karate Demo](karate-demo) has a working example of this set-up.

## Logging
> This is optional, and Karate will work without the logging config in place, but the default
Expand Down
2 changes: 1 addition & 1 deletion karate-core/src/main/java/com/intuit/karate/FileUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public static File getDirContaining(Class clazz) {
return new File(resourcePath).getParentFile();
}

private static URL toFileUrl(String path) {
public static URL toFileUrl(String path) {
path = StringUtils.trim(path);
File file = new File(path);
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@
import cucumber.runtime.xstream.LocalizedXStreams;
import gherkin.formatter.Formatter;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -92,7 +94,7 @@ public List<CucumberFeature> getFeatures() {
}
return list;
}

public List<FeatureFile> getFeatureFiles() {
return featureFiles;
}
Expand All @@ -104,7 +106,7 @@ public RuntimeOptions getRuntimeOptions() {
public ClassLoader getClassLoader() {
return classLoader;
}

public Runtime getRuntime(CucumberFeature feature) {
return getRuntime(new FeatureFile(feature, new File(feature.getPath())));
}
Expand All @@ -119,7 +121,7 @@ public Runtime getRuntime(FeatureFile featureFile) {
}
logger.debug("loading feature: {}", featurePath);
File featureDir = new File(featurePath).getParentFile();
ScriptEnv env = new ScriptEnv(false, null, featureDir, packageFile.getName(), classLoader);
ScriptEnv env = new ScriptEnv(false, null, featureDir, packageFile.getName(), classLoader);
Backend backend = new KarateBackend(env, null, null);
RuntimeGlue glue = new RuntimeGlue(new UndefinedStepsTracker(), new LocalizedXStreams(classLoader));
return new Runtime(resourceLoader, classLoader, Collections.singletonList(backend), runtimeOptions, StopWatch.SYSTEM, glue);
Expand All @@ -132,43 +134,74 @@ public void finish() {
formatter.close();
}

public void run(FeatureFile featureFile, KaratePrettyFormatter formatter) {
public void run(FeatureFile featureFile, KarateJunitFormatter formatter) {
Runtime runtime = getRuntime(featureFile);
featureFile.feature.run(formatter, formatter, runtime);
}

public void run(KaratePrettyFormatter formatter) {
public void run(KarateJunitFormatter formatter) {
for (FeatureFile featureFile : getFeatureFiles()) {
run(featureFile, formatter);
}
}

public static void parallel(Class clazz, int threads) {
ExecutorService executor = Executors.newFixedThreadPool(threads);
private static KarateJunitFormatter getFormatter(String reportDirPath, FeatureFile featureFile) {
File reportDir = new File(reportDirPath);
try {
FileUtils.forceMkdirParent(reportDir);
} catch (Exception e) {
throw new RuntimeException(e);
}
String featurePath = featureFile.feature.getPath();
if (featurePath == null) {
featurePath = featureFile.file.getPath();
}
String featurePackagePath = featurePath.replace(File.separator, ".");
if (featurePackagePath.endsWith(".feature")) {
featurePackagePath = featurePackagePath.substring(0, featurePackagePath.length() - 8);
}
try {
reportDirPath = reportDir.getPath() + File.separator;
String reportPath = reportDirPath + "TEST-" + featurePackagePath + ".xml";
return new KarateJunitFormatter(featurePath, reportPath);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static KarateStats parallel(Class clazz, int threadCount) {
KarateStats stats = KarateStats.startTimer();
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CucumberRunner runner = new CucumberRunner(clazz);
List<FeatureFile> featureFiles = runner.getFeatureFiles();
List<Callable<KaratePrettyFormatter>> callables = new ArrayList<>(featureFiles.size());
for (FeatureFile featureFile : featureFiles) {
KaratePrettyFormatter formatter = new KaratePrettyFormatter();
List<Callable<KarateJunitFormatter>> callables = new ArrayList<>(featureFiles.size());
String reportDir = "target/surefire-reports/";
int count = featureFiles.size();
for (int i = 0; i < count; i++) {
int index = i + 1;
FeatureFile featureFile = featureFiles.get(i);
callables.add(() -> {
String threadName = Thread.currentThread().getName();
KarateJunitFormatter formatter = getFormatter(reportDir, featureFile);
logger.info("START: feature {} out of {} on thread {}: {}", index, count, threadName, featureFile.feature.getPath());
runner.run(featureFile, formatter);
logger.info("=END=: feature {} out of {} on thread {}: {}", index, count, threadName, featureFile.feature.getPath());
formatter.done();
return formatter;
});
}
try {
int scenariosRun = 0;
int scenariosFailed = 0;
List<Future<KaratePrettyFormatter>>
futures = executor.invokeAll(callables);
for (Future<KaratePrettyFormatter > future : futures) {
KaratePrettyFormatter formatter = future.get();
scenariosRun += formatter.getScenariosRun();
scenariosFailed += formatter.getScenariosFailed();
System.out.print(formatter.getBuffer());
List<Future<KarateJunitFormatter>> futures = executor.invokeAll(callables);
stats.stopTimer();
for (Future<KarateJunitFormatter> future : futures) {
KarateJunitFormatter formatter = future.get();
stats.addToTestCount(formatter.getTestCount());
stats.addToFailCount(formatter.getFailCount());
stats.addToSkipCount(formatter.getSkipCount());
stats.addToTimeTaken(formatter.getTimeTaken());
}
System.out.println("==============================");
System.out.println("Scenarios: " + scenariosRun + ", Failed: " + scenariosFailed);
System.out.println("==============================");
stats.printStats(threadCount);
return stats;
} catch (Exception e) {
throw new RuntimeException(e);
}
Expand Down
Loading

0 comments on commit 51dce38

Please sign in to comment.