Skip to content

Commit

Permalink
karate standalone jar can now run multiple features and emit reports
Browse files Browse the repository at this point in the history
and a breaking change to command line arguments, -t is for tags and features are option-less parameters
which can be at the end of the command line and behave like cucumber.options, even paths will be recursively searched for feature files
also the parallel runner is in place and threads are set via -T / --threads
at some point I expect the maven-cucumber-report code to be rolled into the standalone, but for now the JUnit XML and JSON are emitted
which means that the standalone executable may be ready for CI ref #370
  • Loading branch information
ptrthomas committed May 18, 2018
1 parent eafb856 commit 79d518f
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 45 deletions.
7 changes: 3 additions & 4 deletions karate-core/src/main/java/com/intuit/karate/FileUtils.java
Expand Up @@ -147,14 +147,13 @@ private static InputStream getFileStream(String path, PathPrefix prefix, ScriptC
}
}

public static File resolveIfClassPath(String path) {
public static File resolveIfClassPath(String path, ClassLoader classLoader) {
File file = new File(path);
if (file.exists()) { // loaded by karate
return file;
} else { // was loaded by cucumber-jvm, is relative to classpath
String temp = file.getPath().replace('\\', '/'); // fix for windows
ClassLoader cl = Thread.currentThread().getContextClassLoader();
String actualPath = cl.getResource(temp).getFile();
String temp = file.getPath().replace('\\', '/'); // fix for windows
String actualPath = classLoader.getResource(temp).getFile();
return new File(actualPath);
}
}
Expand Down
Expand Up @@ -59,11 +59,15 @@ public static KarateStats parallel(Class clazz, int threadCount) {
}

public static KarateStats parallel(Class clazz, int threadCount, String reportDir) {
KarateRuntimeOptions kro = new KarateRuntimeOptions(clazz);
List<KarateFeature> karateFeatures = KarateFeature.loadFeatures(kro);
return parallel(karateFeatures, threadCount, reportDir);
}

public static KarateStats parallel(List<KarateFeature> karateFeatures, int threadCount, String reportDir) {
KarateStats stats = KarateStats.startTimer();
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
try {
KarateRuntimeOptions kro = new KarateRuntimeOptions(clazz);
List<KarateFeature> karateFeatures = KarateFeature.loadFeatures(kro);
int count = karateFeatures.size();
int filteredCount = 0;
List<Callable<KarateJunitAndJsonReporter>> callables = new ArrayList<>(count);
Expand Down Expand Up @@ -141,22 +145,22 @@ private static void filterOnTags(CucumberFeature feature) throws TagFilterExcept
}
}
}
public static Map<String, Object> runFeature(File file, Map<String, Object> vars, boolean evalKarateConfig) {

public static Map<String, Object> runFeature(File file, Map<String, Object> vars, boolean evalKarateConfig) {
CallContext callContext = new CallContext(vars, evalKarateConfig);
return runFeature(file, callContext);
}
return runFeature(file, callContext, null);
}

public static Map<String, Object> runFeature(File file, CallContext callContext) {
FeatureWrapper featureWrapper = FeatureWrapper.fromFile(file);
public static Map<String, Object> runFeature(File file, CallContext callContext, KarateReporter reporter) {
FeatureWrapper featureWrapper = FeatureWrapper.fromFile(file, reporter);
ScriptValueMap scriptValueMap = CucumberUtils.callSync(featureWrapper, callContext);
return scriptValueMap.toPrimitiveMap();
}

public static Map<String, Object> runFeature(Class relativeTo, String path, Map<String, Object> vars, boolean evalKarateConfig) {
File file = FileUtils.getFileRelativeTo(relativeTo, path);
return runFeature(file, vars, evalKarateConfig);
}
}

public static Map<String, Object> runClasspathFeature(String classPath, Map<String, Object> vars, boolean evalKarateConfig) {
URL url = Thread.currentThread().getContextClassLoader().getResource(classPath);
Expand Down
Expand Up @@ -57,12 +57,16 @@ public void setEnv(ScriptEnv scriptEnv) {
}

public static FeatureWrapper fromFile(File file) {
return fromFile(file, Thread.currentThread().getContextClassLoader());
return fromFile(file, Thread.currentThread().getContextClassLoader(), null);
}

public static FeatureWrapper fromFile(File file, KarateReporter reporter) {
return fromFile(file, Thread.currentThread().getContextClassLoader(), reporter);
}

public static FeatureWrapper fromFile(File file, ClassLoader classLoader) {
public static FeatureWrapper fromFile(File file, ClassLoader classLoader, KarateReporter reporter) {
String text = FileUtils.toString(file);
ScriptEnv env = ScriptEnv.init(file.getParentFile(), file.getName(), classLoader);
ScriptEnv env = new ScriptEnv(null, file.getParentFile(), file.getName(), classLoader, reporter);
return new FeatureWrapper(text, env, file.getPath());
}

Expand Down
Expand Up @@ -44,7 +44,7 @@ public CucumberFeature getFeature() {
}

public KarateFeature(CucumberFeature feature, KarateRuntimeOptions karateOptions) {
file = FileUtils.resolveIfClassPath(feature.getPath());
file = FileUtils.resolveIfClassPath(feature.getPath(), karateOptions.getClassLoader());
this.feature = feature;
this.runtimeOptions = karateOptions;
}
Expand Down
Expand Up @@ -24,6 +24,7 @@
package com.intuit.karate.cucumber;

import com.intuit.karate.CallContext;
import com.intuit.karate.FileUtils;
import com.intuit.karate.ScriptEnv;
import cucumber.runtime.RuntimeGlue;
import cucumber.runtime.RuntimeOptions;
Expand All @@ -34,6 +35,8 @@
import cucumber.runtime.model.CucumberFeature;
import cucumber.runtime.xstream.LocalizedXStreams;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
Expand All @@ -54,10 +57,22 @@ public KarateRuntimeOptions(Class clazz) {
}

public KarateRuntimeOptions(File file) {
classLoader = Thread.currentThread().getContextClassLoader();
runtimeOptions = new RuntimeOptions(file.getPath());
this(null, Arrays.asList(file.getPath()));
}

public KarateRuntimeOptions(String tagOptions, List<String> files) {
classLoader = FileUtils.createClassLoader(files.toArray(new String[]{}));
if (tagOptions != null) {
List<String> temp = new ArrayList();
temp.add("-t");
temp.add(tagOptions);
temp.addAll(files);
runtimeOptions = new RuntimeOptions(temp);
} else {
runtimeOptions = new RuntimeOptions(files);
}
resourceLoader = new MultiLoader(classLoader);
}
}

public KarateRuntime getRuntime(File file, KarateReporter reporter) {
File featureDir = file.getParentFile();
Expand Down
30 changes: 22 additions & 8 deletions karate-netty/README.md
Expand Up @@ -118,30 +118,44 @@ java -jar karate-netty-<version>-all.jar -m my-mock.feature -p 8443 -c my-cert.c
```

### Run Test
Convenient to run a standard [Karate](https://github.com/intuit/karate) test on the command-line without needing to mess around with Java or the IDE ! Great for demos or exploratory testing.
Convenient to run standard [Karate](https://github.com/intuit/karate) tests on the command-line without needing to mess around with Java or the IDE ! Great for demos or exploratory testing.

> Note that if you are depending on external Java libraries or custom code to be compiled, this won't work.
Feature files (or search paths) to be tested don't need command-line options and can be just listed at the end of the command.

```
java -jar karate-netty-<version>-all.jar my-test.feature
```

You can specify [Cucumber tags](https://github.com/intuit/karate#cucumber-tags) to include (or exclude) using the `-t` or `--tags` option as follows:

```
java -jar karate-netty-<version>-all.jar -t my-test.feature
java -jar karate-netty-<version>-all.jar -t @smoke,~@ignore my-test.feature
```

If your test depends on the `karate.env` [environment 'switch'](https://github.com/intuit/karate#switching-the-environment), you can specify that using the `-e` (env) option:

```
java -jar karate-netty-<version>-all.jar -t my-test.feature -e e2e
java -jar karate-netty-<version>-all.jar -e e2e my-test.feature
```

If [`karate-config.js`](https://github.com/intuit/karate#configuration) exists in the current working directory, it will be used. You can specify a full path by setting the system property `karate.config`. Note that this is an easy way to set a bunch of variables, just return a JSON with the keys and values you need.

```
java -jar -Dkarate.config=somedir/my-config.js karate-netty-<version>-all.jar -t my-test.feature
java -jar -Dkarate.config=somedir/my-config.js karate-netty-<version>-all.jar my-test.feature
```

And you can even set or over-ride variable values via the command line by using the `-a` (args) option:

```
java -jar karate-netty-<version>-all.jar -t my-test.feature -a myKey1=myValue1 -a myKey2=myValue2
java -jar karate-netty-<version>-all.jar -a myKey1=myValue1 -a myKey2=myValue2 my-test.feature
```

If you provide a directory in which multiple feature files are present (even in sub-folders), they will be all run. You can even specify the number of threads to run in parallel using `-T` or `--threads` (not to be confused with `-t` for tags):

```
java -jar karate-netty-<version>-all.jar -T 5 -t ~@ignore src/features
```

### UI
Expand All @@ -152,15 +166,15 @@ java -jar karate-netty-<version>-all.jar

You can also open an existing Karate test in the UI via the command-line:
```
java -jar karate-netty-<version>-all.jar -u -t my-test.feature
java -jar karate-netty-<version>-all.jar -u my-test.feature
```

## Logging
A default [logback configuration file](https://logback.qos.ch/manual/configuration.html) (named [`logback-netty.xml`](src/main/resources/logback-netty.xml)) is present within the stand-alone JAR. If you need to customize logging, set the system property `logback.configurationFile` to point to your custom config:
```
java -jar -Dlogback.configurationFile=my-logback.xml karate-netty-<version>-all.jar -t my-test.feature
```
Here is the 'out-of-the-box' default which you can customize.
Here is the 'out-of-the-box' default which you can customize. Note that the default creates a folder called `target` and within it, logs will be in `karate.log`.

```xml
<?xml version="1.0" encoding="UTF-8"?>
Expand All @@ -173,7 +187,7 @@ Here is the 'out-of-the-box' default which you can customize.
</appender>

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>karate.log</file>
<file>target/karate.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
Expand Down
38 changes: 24 additions & 14 deletions karate-netty/src/main/java/com/intuit/karate/netty/Main.java
Expand Up @@ -4,7 +4,7 @@
* Copyright 2018 Intuit Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* of this software and associated documentation tests (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
Expand All @@ -27,6 +27,9 @@
import com.intuit.karate.ScriptBindings;
import com.intuit.karate.StringUtils;
import com.intuit.karate.cucumber.CucumberRunner;
import com.intuit.karate.cucumber.KarateFeature;
import com.intuit.karate.cucumber.KarateRuntimeOptions;
import com.intuit.karate.cucumber.KarateStats;
import com.intuit.karate.exception.KarateException;
import com.intuit.karate.ui.App;
import io.netty.handler.ssl.util.SelfSignedCertificate;
Expand All @@ -40,6 +43,7 @@
import picocli.CommandLine.DefaultExceptionHandler;
import picocli.CommandLine.ExecutionException;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.ParseResult;
import picocli.CommandLine.RunLast;

Expand Down Expand Up @@ -74,8 +78,14 @@ public class Main implements Callable<Void> {
@Option(names = {"-k", "--key"}, description = "ssl private key (default: " + KEY_FILE + ")")
File key;

@Option(names = {"-t", "--test"}, description = "run feature file as Karate test")
File test;
@Option(names = {"-t", "--tags"}, description = "cucumber tags - e.g. '@smoke,~@ignore'")
String tags;

This comment has been minimized.

Copy link
@slaout

slaout Jun 22, 2018

Contributor

Shouldn't it be a List?

Cucumber allows to run tags "-t @A,@b -t @c" for the expression "(tag a OR tag b) AND tag c".

Note that is Cucumber 2, such tag selection has been entirely reworked for a full boolean operators expression:
https://github.com/cucumber/cucumber/tree/master/tag-expressions


@Option(names = {"-T", "--threads"}, description = "number of threads when running tests")
int threads = 1;

@Parameters(description = "one or more tests (features) or search-paths to run")
List<String> tests;

@Option(names = {"-e", "--env"}, description = "value of 'karate.env'")
String env;
Expand All @@ -96,22 +106,22 @@ public static void main(String[] args) {
CommandLine cmd = new CommandLine(new Main());
DefaultExceptionHandler<List<Object>> exceptionHandler = new DefaultExceptionHandler() {
@Override
public Object handleExecutionException(ExecutionException ex, ParseResult parseResult) {
public Object handleExecutionException(ExecutionException ex, ParseResult parseResult) {
if (ex.getCause() instanceof KarateException) {
throw new ExecutionException(cmd, ""); // minimum possible stack trace but exit code 1
throw new ExecutionException(cmd, ex.getCause().getMessage()); // minimum possible stack trace but exit code 1
} else {
throw ex;
}
}
};
}
};
cmd.parseWithHandlers(new RunLast(), exceptionHandler, args);
}

@Override
public Void call() throws Exception {
if (test != null) {
if (tests != null) {
if (ui) {
App.main(new String[]{test.getAbsolutePath(), env});
App.main(new String[]{tests.get(0), env});
} else {
if (env != null) {
System.setProperty(ScriptBindings.KARATE_ENV, env);
Expand All @@ -120,11 +130,11 @@ public Void call() throws Exception {
if (configPath == null) {
System.setProperty(ScriptBindings.KARATE_CONFIG, new File(ScriptBindings.KARATE_CONFIG_JS).getPath() + "");
}
try {
CucumberRunner.runFeature(test, args, true);
} catch (Exception e) {
logger.error(e.getMessage());
throw new KarateException(e.getMessage());
KarateRuntimeOptions kro = new KarateRuntimeOptions(tags, tests);
List<KarateFeature> karateFeatures = KarateFeature.loadFeatures(kro);
KarateStats stats = CucumberRunner.parallel(karateFeatures, threads, "target");
if (stats.getFailCount() > 0) {
throw new KarateException("there are test failures");
}
}
return null;
Expand Down
2 changes: 1 addition & 1 deletion karate-netty/src/main/resources/logback-netty.xml
Expand Up @@ -8,7 +8,7 @@
</appender>

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>karate.log</file>
<file>target/karate.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
Expand Down
Expand Up @@ -11,7 +11,7 @@ public class MainTestRunner {
@Test
public void testMain() {
System.setProperty("karate.config", "src/test/java/karate-config.js");
Main.main(new String[]{"-t", "src/test/java/com/intuit/karate/netty/client.feature"});
Main.main(new String[]{"-t", "~@ignore", "src/test/java/com/intuit/karate/netty"});
}

}
@@ -1,3 +1,4 @@
@ignore
Feature:

Background:
Expand Down
Expand Up @@ -49,7 +49,7 @@ public void tearDownClass() throws Exception {
@Test(groups = "cucumber", description = "Runs Cucumber Feature", dataProvider = "features")
public void feature(CucumberFeatureWrapper wrapper) {
CucumberFeature feature = wrapper.getCucumberFeature();
File file = FileUtils.resolveIfClassPath(feature.getPath());
File file = FileUtils.resolveIfClassPath(feature.getPath(), runtimeOptions.getClassLoader());
KarateRuntime runtime = runtimeOptions.getRuntime(file, null);
resultListener.startFeature();
RuntimeOptions ro = runtimeOptions.getRuntimeOptions();
Expand Down

0 comments on commit 79d518f

Please sign in to comment.