Skip to content

Commit

Permalink
Merge pull request #597 from friederbluemle/android-cmd-line-options
Browse files Browse the repository at this point in the history
Add command line option support for Android
  • Loading branch information
SierraGolf committed Nov 2, 2013
2 parents 45d0055 + ce52f30 commit d147a9e
Show file tree
Hide file tree
Showing 3 changed files with 357 additions and 28 deletions.
131 changes: 103 additions & 28 deletions android/src/main/java/cucumber/api/android/CucumberInstrumentation.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import cucumber.runtime.android.AndroidLogcatReporter;
import cucumber.runtime.android.AndroidObjectFactory;
import cucumber.runtime.android.AndroidResourceLoader;
import cucumber.runtime.android.InstrumentationArguments;
import cucumber.runtime.android.DexClassFinder;
import cucumber.runtime.android.TestCaseCounter;
import cucumber.runtime.io.ResourceLoader;
Expand All @@ -28,28 +29,36 @@
import gherkin.formatter.Formatter;
import gherkin.formatter.Reporter;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class CucumberInstrumentation extends Instrumentation {
public static final String REPORT_VALUE_ID = "CucumberInstrumentation";
public static final String REPORT_KEY_NUM_TOTAL = "numtests";
private static final String REPORT_KEY_COVERAGE_PATH = "coverageFilePath";
private static final String DEFAULT_COVERAGE_FILE_NAME = "coverage.ec";
public static final String TAG = "cucumber-android";

private final Bundle results = new Bundle();
private int testCount;

private RuntimeOptions runtimeOptions;
private ResourceLoader resourceLoader;
private ClassLoader classLoader;
private Runtime runtime;
private boolean debug;
private List<CucumberFeature> cucumberFeatures;
InstrumentationArguments instrumentationArguments;

@Override
public void onCreate(Bundle arguments) {
super.onCreate(arguments);

if (arguments == null) {
throw new CucumberException("No arguments");
}

debug = getBooleanArgument(arguments, "debug");

instrumentationArguments = new InstrumentationArguments(arguments);

Context context = getContext();
classLoader = context.getClassLoader();

Expand All @@ -60,6 +69,7 @@ public void onCreate(Bundle arguments) {
for (Class<?> clazz : classFinder.getDescendants(Object.class, context.getPackageName())) {
if (clazz.isAnnotationPresent(CucumberOptions.class)) {
Log.d(TAG, "Found CucumberOptions in class " + clazz.getName());
Log.d(TAG, clazz.getAnnotations()[0].toString());
optionsAnnotatedClass = clazz;
break; // We assume there is only one CucumberOptions annotated class.
}
Expand All @@ -68,6 +78,12 @@ public void onCreate(Bundle arguments) {
throw new CucumberException("No CucumberOptions annotation");
}

String cucumberOptions = instrumentationArguments.getCucumberOptionsString();
if (!cucumberOptions.isEmpty()) {
Log.d(TAG, "Setting cucumber.options from arguments: '" + cucumberOptions + "'");
System.setProperty("cucumber.options", cucumberOptions);
}

@SuppressWarnings("unchecked")
RuntimeOptionsFactory factory = new RuntimeOptionsFactory(optionsAnnotatedClass, new Class[]{CucumberOptions.class});
runtimeOptions = factory.create();
Expand All @@ -78,6 +94,8 @@ public void onCreate(Bundle arguments) {
AndroidObjectFactory objectFactory = new AndroidObjectFactory(delegateObjectFactory, this);
backends.add(new JavaBackend(objectFactory, classFinder));
runtime = new Runtime(resourceLoader, classLoader, backends, runtimeOptions);
cucumberFeatures = runtimeOptions.cucumberFeatures(resourceLoader);
testCount = TestCaseCounter.countTestCasesOf(cucumberFeatures);

start();
}
Expand All @@ -93,30 +111,37 @@ private DexFile newDexFile(String apkPath) {
@Override
public void onStart() {
Looper.prepare();

if (debug) {
Debug.waitForDebugger();
}

final List<CucumberFeature> cucumberFeatures = runtimeOptions.cucumberFeatures(resourceLoader);
final int numberOfTests = TestCaseCounter.countTestCasesOf(cucumberFeatures);
if (instrumentationArguments.isCountEnabled()) {
results.putString(Instrumentation.REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID);
results.putInt(REPORT_KEY_NUM_TOTAL, testCount);
finish(Activity.RESULT_OK, results);
} else {
if (instrumentationArguments.isDebugEnabled()) {
Debug.waitForDebugger();
}

runtimeOptions.getFormatters().add(new AndroidInstrumentationReporter(runtime, this, numberOfTests));
runtimeOptions.getFormatters().add(new AndroidLogcatReporter(TAG));
runtimeOptions.getFormatters().add(new AndroidInstrumentationReporter(runtime, this, testCount));
runtimeOptions.getFormatters().add(new AndroidLogcatReporter(TAG));

final Reporter reporter = runtimeOptions.reporter(classLoader);
final Formatter formatter = runtimeOptions.formatter(classLoader);
final Reporter reporter = runtimeOptions.reporter(classLoader);
final Formatter formatter = runtimeOptions.formatter(classLoader);

for (final CucumberFeature cucumberFeature : cucumberFeatures) {
cucumberFeature.run(formatter, reporter, runtime);
}
for (final CucumberFeature cucumberFeature : cucumberFeatures) {
cucumberFeature.run(formatter, reporter, runtime);
}

formatter.done();
formatter.close();

formatter.done();
formatter.close();
printSummary();

printSummary();
if (instrumentationArguments.isCoverageEnabled()) {
generateCoverageReport();
}

finish(Activity.RESULT_OK, new Bundle());
finish(Activity.RESULT_OK, results);
}
}

private void printSummary() {
Expand All @@ -128,9 +153,59 @@ private void printSummary() {
Log.w(TAG, s);
}
}

private boolean getBooleanArgument(Bundle arguments, String tag) {
String tagString = arguments.getString(tag);
return tagString != null && Boolean.parseBoolean(tagString);

private void generateCoverageReport() {
// use reflection to call emma dump coverage method, to avoid
// always statically compiling against emma jar
String coverageFilePath = getCoverageFilePath();
java.io.File coverageFile = new java.io.File(coverageFilePath);
try {
Class<?> emmaRTClass = Class.forName("com.vladium.emma.rt.RT");
Method dumpCoverageMethod = emmaRTClass.getMethod("dumpCoverageData",
coverageFile.getClass(), boolean.class, boolean.class);

dumpCoverageMethod.invoke(null, coverageFile, false, false);
// output path to generated coverage file so it can be parsed by a test harness if
// needed
results.putString(REPORT_KEY_COVERAGE_PATH, coverageFilePath);
// also output a more user friendly msg
final String currentStream = results.getString(
Instrumentation.REPORT_KEY_STREAMRESULT);
results.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
String.format("%s\nGenerated code coverage data to %s", currentStream,
coverageFilePath));
} catch (ClassNotFoundException e) {
reportEmmaError("Is emma jar on classpath?", e);
} catch (SecurityException e) {
reportEmmaError(e);
} catch (NoSuchMethodException e) {
reportEmmaError(e);
} catch (IllegalArgumentException e) {
reportEmmaError(e);
} catch (IllegalAccessException e) {
reportEmmaError(e);
} catch (InvocationTargetException e) {
reportEmmaError(e);
}
}

private String getCoverageFilePath() {
String coverageFilePath = instrumentationArguments.getCoverageFilePath();
if (coverageFilePath == null) {
return getTargetContext().getFilesDir().getAbsolutePath() + File.separator +
DEFAULT_COVERAGE_FILE_NAME;
} else {
return coverageFilePath;
}
}

private void reportEmmaError(Exception e) {
reportEmmaError("", e);
}

private void reportEmmaError(String hint, Exception e) {
String msg = "Failed to generate emma coverage. " + hint;
Log.e(TAG, msg, e);
results.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "\nError: " + msg);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package cucumber.runtime.android;

import android.os.Bundle;

/**
* This is a wrapper class around the command line arguments that were supplied
* when the instrumentation was started.
*/
public final class InstrumentationArguments {
private static final String KEY_DEBUG = "debug";
private static final String KEY_LOG = "log";
private static final String KEY_COUNT = "count";
private static final String KEY_COVERAGE = "coverage";
private static final String KEY_COVERAGE_FILE_PATH = "coverageFile";
private static final String VALUE_SEPARATOR = "--";

private Bundle arguments;

public InstrumentationArguments(Bundle arguments) {
this.arguments = arguments != null ? arguments : new Bundle();
}

private boolean getBooleanArgument(String tag) {
String tagString = arguments.getString(tag);
return tagString != null && Boolean.parseBoolean(tagString);
}

private void appendOption(StringBuilder sb, String optionKey, String optionValue) {
for (String value : optionValue.split(VALUE_SEPARATOR)) {
sb.append(sb.length() == 0 || optionKey.isEmpty() ? "" : " ").append(optionKey).append(optionValue.isEmpty() ? "" : " " + value);
}
}

/**
* Returns a Cucumber options compatible string based on the argument extras found.
* <p />
* The bundle <em>cannot</em> contain multiple entries for the same key,
* however certain Cucumber options can be passed multiple times (e.g.
* {@code --tags}). The solution is to pass values separated by
* {@link InstrumentationArguments#VALUE_SEPARATOR} which will result
* in multiple {@code --key value} pairs being created.
*
* @return the cucumber options string
*/
public String getCucumberOptionsString() {
String cucumberOptions = arguments.getString("cucumberOptions");
if (cucumberOptions != null) {
return cucumberOptions;
}

StringBuilder sb = new StringBuilder();
String features = "";
for (String key : arguments.keySet()) {
if ("glue".equals(key)) {
appendOption(sb, "--glue", arguments.getString(key));
} else if ("format".equals(key)) {
appendOption(sb, "--format", arguments.getString(key));
} else if ("tags".equals(key)) {
appendOption(sb, "--tags", arguments.getString(key));
} else if ("name".equals(key)) {
appendOption(sb, "--name", arguments.getString(key));
} else if ("dryRun".equals(key) && getBooleanArgument(key)) {
appendOption(sb, "--dry-run", "");
} else if ("log".equals(key) && getBooleanArgument(key)) {
appendOption(sb, "--dry-run", "");
} else if ("noDryRun".equals(key) && getBooleanArgument(key)) {
appendOption(sb, "--no-dry-run", "");
} else if ("monochrome".equals(key) && getBooleanArgument(key)) {
appendOption(sb, "--monochrome", "");
} else if ("noMonochrome".equals(key) && getBooleanArgument(key)) {
appendOption(sb, "--no-monochrome", "");
} else if ("strict".equals(key) && getBooleanArgument(key)) {
appendOption(sb, "--strict", "");
} else if ("noStrict".equals(key) && getBooleanArgument(key)) {
appendOption(sb, "--no-strict", "");
} else if ("snippets".equals(key)) {
appendOption(sb, "--snippets", arguments.getString(key));
} else if ("dotcucumber".equals(key)) {
appendOption(sb, "--dotcucumber", arguments.getString(key));
} else if ("features".equals(key)) {
features = arguments.getString(key);
}
}
// Even though not strictly required, wait until everything else
// has been added before adding any feature references
appendOption(sb, "", features);
return sb.toString();
}

public boolean isDebugEnabled() {
return getBooleanArgument(KEY_DEBUG);
}

public boolean isLogEnabled() {
return getBooleanArgument(KEY_LOG);
}

public boolean isCountEnabled() {
return getBooleanArgument(KEY_COUNT);
}

public boolean isCoverageEnabled() {
return getBooleanArgument(KEY_COVERAGE);
}

public String getCoverageFilePath() {
return arguments.getString(KEY_COVERAGE_FILE_PATH);
}
}
Loading

0 comments on commit d147a9e

Please sign in to comment.