diff --git a/README.md b/README.md
index 6faf121..de0e2f7 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,11 @@ You can also use command line to provide these options to cucumber-android. Here
android.defaultConfig.testInstrumentationRunner "cucumber.cukeulator.test.CukeulatorAndroidJUnitRunner"
```
+### File-based tests
+
+If, for some reason, you need to load features from the filesystem, use an absolute path when specifying the features options to @CucumberOptions (e.g. `features = "/data/local/tmp"`).
+
+It is up to you to ensure your instrumentation has permission to read from the filesystem. You may need to add something like `` to your AndroidManifest.xml.
### Debugging
Please read [the Android documentation on debugging](https://developer.android.com/tools/debugging/index.html).
diff --git a/cucumber-android/src/main/java/cucumber/runtime/android/AndroidResourceLoader.java b/cucumber-android/src/main/java/cucumber/runtime/android/AndroidResourceLoader.java
index fd1b590..8bcac3d 100644
--- a/cucumber-android/src/main/java/cucumber/runtime/android/AndroidResourceLoader.java
+++ b/cucumber-android/src/main/java/cucumber/runtime/android/AndroidResourceLoader.java
@@ -5,6 +5,7 @@
import cucumber.runtime.CucumberException;
import cucumber.runtime.io.Resource;
import cucumber.runtime.io.ResourceLoader;
+import cucumber.runtime.io.MultiLoader;
import java.io.IOException;
import java.net.URI;
@@ -26,17 +27,37 @@ final class AndroidResourceLoader implements ResourceLoader {
*/
private final Context context;
+ /**
+ * The {@link MultiLoader} to get the resources from.
+ */
+ private final MultiLoader multiLoader;
+
/**
* Creates a new instance for the given parameter.
*
* @param context the {@link Context} to get resources from
*/
AndroidResourceLoader(final Context context) {
+ this(context, new MultiLoader(context.getClassLoader()));
+ }
+
+ /**
+ * Creates a new instance for the given parameter.
+ *
+ * @param context the {@link Context} to get resources from
+ * @param multiLoader the {@link Context} to get resources from
+ */
+ AndroidResourceLoader(final Context context, final MultiLoader multiLoader) {
this.context = context;
+ this.multiLoader = multiLoader;
}
@Override
public Iterable resources(final URI path, final String suffix) {
+ if (path.getScheme().equals("file") && path.getRawSchemeSpecificPart().startsWith("/")) {
+ return multiLoader.resources(path, suffix);
+ }
+
try {
final List resources = new ArrayList<>();
final AssetManager assetManager = context.getAssets();
diff --git a/cucumber-android/src/test/java/cucumber/runtime/android/AndroidResourceLoaderTest.java b/cucumber-android/src/test/java/cucumber/runtime/android/AndroidResourceLoaderTest.java
index 1ca87bc..4f301ee 100644
--- a/cucumber-android/src/test/java/cucumber/runtime/android/AndroidResourceLoaderTest.java
+++ b/cucumber-android/src/test/java/cucumber/runtime/android/AndroidResourceLoaderTest.java
@@ -3,6 +3,7 @@
import android.content.Context;
import android.content.res.AssetManager;
import com.google.common.collect.Lists;
+import cucumber.runtime.io.MultiLoader;
import cucumber.runtime.io.Resource;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
@@ -15,6 +16,7 @@
import java.io.IOException;
import java.net.URI;
+import java.util.Iterator;
import java.util.List;
import static org.hamcrest.CoreMatchers.hasItem;
@@ -30,7 +32,12 @@ public class AndroidResourceLoaderTest {
private final Context context = mock(Context.class);
private final AssetManager assetManager = mock(AssetManager.class, RETURNS_SMART_NULLS);
- private final AndroidResourceLoader androidResourceLoader = new AndroidResourceLoader(context);
+ private final MultiLoader multiLoader = mock(MultiLoader.class, RETURNS_SMART_NULLS);
+ private final Resource fileResource1 = mock(Resource.class, RETURNS_SMART_NULLS);
+ private final Resource fileResource2 = mock(Resource.class, RETURNS_SMART_NULLS);
+ private Iterable fileResources = mock(Iterable.class, RETURNS_SMART_NULLS);
+ private Iterator fileResourcesIterator = mock(Iterator.class, RETURNS_SMART_NULLS);
+ private final AndroidResourceLoader androidResourceLoader = new AndroidResourceLoader(context, multiLoader);
@Before
public void beforeEachTest() {
@@ -94,6 +101,81 @@ public void only_retrieves_those_resources_which_end_the_specified_suffix() thro
assertThat(resources, hasItem(withPath(path + "/" + expected)));
}
+ @Test
+ public void retrieves_file_resources_with_an_absolute_path() throws IOException {
+ // given
+ final String path = "file:/dir";
+ final String dir = "dir";
+ final String dirFile = "dir.feature";
+ final String subDir = "subdir";
+ final String subDirFile = "subdir.feature";
+ final String suffix = "feature";
+
+ when(fileResource1.getPath()).thenReturn(URI.create(path + "/" + dirFile));
+ when(fileResource2.getPath()).thenReturn(URI.create(path + "/" + subDir + "/" + subDirFile));
+ when(fileResourcesIterator.hasNext()).thenReturn(Boolean.TRUE, Boolean.TRUE, Boolean.FALSE);
+ when(fileResourcesIterator.next()).thenReturn(fileResource1, fileResource2);
+ when(fileResources.iterator()).thenReturn(fileResourcesIterator);
+ when(multiLoader.resources(URI.create(path), suffix)).thenReturn(fileResources);
+
+ // when
+ final List resources = Lists.newArrayList(androidResourceLoader.resources(URI.create(path), suffix));
+
+ // then
+ assertThat(resources.size(), is(2));
+ assertThat(resources, hasItem(withPath(path + "/" + dirFile)));
+ assertThat(resources, hasItem(withPath(path + "/" + subDir + "/" + subDirFile)));
+ }
+
+ @Test
+ public void only_retrieves_android_resources_with_a_relative_path() throws IOException {
+ // given
+ final String dir = "dir";
+ String absPath = "file:/" + dir;
+ String relPath = "file:" + dir;
+ final String expected = "expected.feature";
+ final String unexpected = "unexpected.feature";
+ final String suffix = "feature";
+
+ when(assetManager.list(dir)).thenReturn(new String[]{expected});
+ when(fileResource1.getPath()).thenReturn(URI.create(absPath + "/" + unexpected));
+ when(fileResourcesIterator.hasNext()).thenReturn(Boolean.TRUE, Boolean.FALSE);
+ when(fileResourcesIterator.next()).thenReturn(fileResource1);
+ when(fileResources.iterator()).thenReturn(fileResourcesIterator);
+ when(multiLoader.resources(URI.create(absPath), suffix)).thenReturn(fileResources);
+
+ // when
+ final List resources = Lists.newArrayList(androidResourceLoader.resources(URI.create(relPath), suffix));
+
+ // then
+ assertThat(resources.size(), is(1));
+ assertThat(resources, hasItem(withPath(relPath + "/" + expected)));
+ }
+
+ @Test
+ public void only_retrieves_file_resources_with_an_absolute_path() throws IOException {
+ // given
+ final String dir = "dir";
+ String absPath = "file:/" + dir;
+ final String expected = "expected.feature";
+ final String unexpected = "unexpected.feature";
+ final String suffix = "feature";
+
+ when(fileResource1.getPath()).thenReturn(URI.create(absPath + "/" + expected));
+ when(fileResourcesIterator.hasNext()).thenReturn(Boolean.TRUE, Boolean.FALSE);
+ when(fileResourcesIterator.next()).thenReturn(fileResource1);
+ when(fileResources.iterator()).thenReturn(fileResourcesIterator);
+ when(multiLoader.resources(URI.create(absPath), suffix)).thenReturn(fileResources);
+ when(assetManager.list(dir)).thenReturn(new String[]{unexpected});
+
+ // when
+ final List resources = Lists.newArrayList(androidResourceLoader.resources(URI.create(absPath), suffix));
+
+ // then
+ assertThat(resources.size(), is(1));
+ assertThat(resources, hasItem(withPath(absPath + "/" + expected)));
+ }
+
private static Matcher super Resource> withPath(final String path) {
return new TypeSafeMatcher() {
@Override