Skip to content

Add command line option support for Android #597

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Nov 2, 2013
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little confused now on what the consensus is about multiple/single argument, but wanted to give a hint about this line of code anyway. I think you can inline this method by simply using arguments.getBoolean(tag) in case the default should be false, or use arguments.getBoolean(tag, true)in case the default should be true.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind, I just double checked and found out that the Bundle helper methods are not working in this context.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, unfortunately arguments.getBoolean(tag, true) cannot be used here, since it casts the value to a Boolean instead of parsing a String.

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