Skip to content

Commit d8406dd

Browse files
gnuechtelbrasmusson
authored andcommitted
Android: Fix Cucumber execution on Gradle (#1094)
* Fix AndroidInstrumentationReporter for Gradle builds * the connected check tasks of the Android/Gradle build system do not like non-unique test names * we add a unique index to the test names if they are non-unique (e.g. on scenario outlines with multiple examples) * for this bug fix, we provide a unit test * Fix AndroidInstrumentationReporter for Gradle builds * the connected check tasks of the Android/Gradle build system do not like non-unique test names * we add a unique index to the test names if they are non-unique (e.g. on scenario outlines with multiple examples) * for this bug fix, we provide a unit test This is a re-integration of PR-1094. The original pull request code was performed on 1.2.6-SNAPSHOT base. This code bases on version 2.3.2-SNAPSHOT. * Update Cukeulator example project to work with newest Android build tools - Update to Android Studio 3.0.1, SDK 26+ and Gradle 4.1 - Replace Instrumentation class by CucumberRunner - CucumberRunner uses AndroidJUnitRunner (JUnit 4+) - CalculationSteps class uses ActivityTestRule instead of deprecated ActivityInstrumentationTestCase2 - Fix permissions to write reports on internal storage * Improve Cukeulator example project * Update README.md * Enable local Maven dependencies for better development experience * Describe, how to use Cukeulator example project with locally built Cucumber-JVM * Rename duplicated test case names in AndroidInstrumentationReporter like JUnitFormatter in cucumber-core * Fix typo * Improve readability of AndroidInstrumentationReporterTest * Share common logic for test case name between JUnitFormatter and AndroidInstrumentationReporter * Improve code quality - Create merged method calculateUniqueTestName from getUniqueTestName and ensureUniqueTestName and make it private - Use better test method name: test_case_names_are_unique_on_equal_scenario_names (instead of scenario_outline_all_test_names_unique) - Refactor test code: now it should be readable * Change misleading variable names * [Android] Split up test of making test names unique. To provide better documentation of the functionality, that is: * test names within feature are made unique by appending blank and number * test names within are made unique by appending underscore and number when no blank in name * test names in different features can be the same * test names are made unique also when not consecutive
1 parent df409a0 commit d8406dd

File tree

12 files changed

+516
-56
lines changed

12 files changed

+516
-56
lines changed

android/src/main/java/cucumber/runtime/formatter/AndroidInstrumentationReporter.java

+55-1
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,15 @@
1212
import cucumber.api.event.TestStepFinished;
1313
import cucumber.api.formatter.Formatter;
1414
import cucumber.runtime.Runtime;
15+
import cucumber.runtime.Utils;
1516

1617
import java.io.PrintWriter;
1718
import java.io.StringWriter;
19+
import java.util.HashMap;
20+
import java.util.HashSet;
21+
import java.util.IdentityHashMap;
22+
import java.util.Map;
23+
import java.util.Set;
1824

1925
/**
2026
* Reports the test results to the instrumentation through {@link Instrumentation#sendStatus(int, Bundle)} calls.
@@ -170,7 +176,8 @@ void startTestCase(final TestCase testCase) {
170176
currentUri = testCase.getUri();
171177
currentFeatureName = testSources.getFeatureName(currentUri);
172178
}
173-
currentTestCaseName = testCase.getName();
179+
// Since the names of test cases are not guaranteed to be unique, we must check for unique names
180+
currentTestCaseName = calculateUniqueTestName(testCase);
174181
resetSeverestResult();
175182
final Bundle testStart = createBundle(currentFeatureName, currentTestCaseName);
176183
instrumentation.sendStatus(StatusCodes.START, testStart);
@@ -277,4 +284,51 @@ private static String getStackTrace(final Throwable throwable) {
277284
throwable.printStackTrace(printWriter);
278285
return stringWriter.getBuffer().toString();
279286
}
287+
288+
/**
289+
* The stored unique test name for a test case.
290+
* We use an identity hash-map since we want to distinct all test case objects.
291+
* Thus, the key is a unique test case object.<br/>
292+
* The mapped value is the unique test name, which maybe differs from test case original non-unique name.
293+
*/
294+
private final Map<TestCase, String> uniqueTestNameForTestCase = new IdentityHashMap<TestCase, String>();
295+
296+
/**
297+
* The stored unique test names grouped by feature.<br/>
298+
* The key contains the feature file.<br/>
299+
* The mapped value is a set of unique test names for the feature.
300+
*/
301+
private final Map<String, Set<String>> uniqueTestNamesForFeature = new HashMap<String, Set<String>>();
302+
303+
/**
304+
* Creates a unique test name for the given test case by filling the internal maps
305+
* {@link #uniqueTestNameForTestCase} and {@link #uniqueTestNamesForFeature}.<br/>
306+
* If the test case name is unique, it will be used, otherwise, a index will be added " 2", " 3", " 4", ...
307+
* @param testCase the test case
308+
* @return a unique test name
309+
*/
310+
private String calculateUniqueTestName(TestCase testCase) {
311+
String existingName = uniqueTestNameForTestCase.get(testCase);
312+
if (existingName != null) {
313+
// Nothing to do: there is already a test name for the passed test case object
314+
return existingName;
315+
}
316+
final String feature = testCase.getUri();
317+
String uniqueTestCaseName = testCase.getName();
318+
if (!uniqueTestNamesForFeature.containsKey(feature)) {
319+
// First test case of the feature
320+
uniqueTestNamesForFeature.put(feature, new HashSet<String>());
321+
}
322+
final Set<String> uniqueTestNamesSetForFeature = uniqueTestNamesForFeature.get(feature);
323+
// If "name" already exists, the next one is "name_2" or "name with spaces 2"
324+
int i = 2;
325+
while (uniqueTestNamesSetForFeature.contains(uniqueTestCaseName)) {
326+
uniqueTestCaseName = Utils.getUniqueTestNameForScenarioExample(testCase.getName(), i);
327+
i++;
328+
}
329+
uniqueTestNamesSetForFeature.add(uniqueTestCaseName);
330+
uniqueTestNameForTestCase.put(testCase, uniqueTestCaseName);
331+
return uniqueTestNameForTestCase.get(testCase);
332+
}
333+
280334
}

android/src/test/java/cucumber/runtime/formatter/AndroidInstrumentationReporterTest.java

+114
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package cucumber.runtime.formatter;
22

3+
import static java.util.Arrays.asList;
34
import static org.hamcrest.CoreMatchers.containsString;
5+
import static org.hamcrest.CoreMatchers.equalTo;
46
import static org.junit.Assert.assertThat;
57
import static org.mockito.Matchers.any;
68
import static org.mockito.Matchers.eq;
@@ -28,6 +30,8 @@
2830
import org.robolectric.RobolectricTestRunner;
2931
import org.robolectric.annotation.Config;
3032

33+
import java.util.List;
34+
3135
@Config(manifest = Config.NONE)
3236
@RunWith(RobolectricTestRunner.class)
3337
public class AndroidInstrumentationReporterTest {
@@ -458,8 +462,118 @@ public void step_result_contains_only_the_current_scenarios_severest_result() {
458462
inOrder.verify(instrumentation).sendStatus(eq(StatusCodes.OK), secondCaptor.capture());
459463
}
460464

465+
@Test
466+
public void test_names_within_feature_are_made_unique_by_appending_blank_and_number() {
467+
468+
// given
469+
final AndroidInstrumentationReporter formatter = new AndroidInstrumentationReporter(runtime, instrumentation);
470+
TestCase testCase1 = mockTestCase(testCaseName("not unique name"));
471+
TestCase testCase2 = mockTestCase(testCaseName("not unique name"));
472+
TestCase testCase3 = mockTestCase(testCaseName("not unique name"));
473+
474+
// when
475+
simulateRunningTestCases(formatter, asList(testCase1, testCase2, testCase3));
476+
477+
// then
478+
final InOrder inOrder = inOrder(instrumentation);
479+
assertThat(captureTestName(inOrder, instrumentation), equalTo("not unique name"));
480+
assertThat(captureTestName(inOrder, instrumentation), equalTo("not unique name 2"));
481+
assertThat(captureTestName(inOrder, instrumentation), equalTo("not unique name 3"));
482+
}
483+
484+
@Test
485+
public void test_names_within_are_made_unique_by_appending_underscore_and_number_when_no_blank_in_name() {
486+
487+
// given
488+
final AndroidInstrumentationReporter formatter = new AndroidInstrumentationReporter(runtime, instrumentation);
489+
TestCase testCase1 = mockTestCase(testCaseName("not_unique_name"));
490+
TestCase testCase2 = mockTestCase(testCaseName("not_unique_name"));
491+
TestCase testCase3 = mockTestCase(testCaseName("not_unique_name"));
492+
493+
// when
494+
simulateRunningTestCases(formatter, asList(testCase1, testCase2, testCase3));
495+
496+
// then
497+
final InOrder inOrder = inOrder(instrumentation);
498+
assertThat(captureTestName(inOrder, instrumentation), equalTo("not_unique_name"));
499+
assertThat(captureTestName(inOrder, instrumentation), equalTo("not_unique_name_2"));
500+
assertThat(captureTestName(inOrder, instrumentation), equalTo("not_unique_name_3"));
501+
}
502+
503+
@Test
504+
public void test_names_in_different_features_can_be_the_same() {
505+
506+
// given
507+
final AndroidInstrumentationReporter formatter = new AndroidInstrumentationReporter(runtime, instrumentation);
508+
TestCase testCase1 = mockTestCase(featureUri("path/file1.feature"), testCaseName("not unique name"));
509+
TestCase testCase2 = mockTestCase(featureUri("path/file2.feature"), testCaseName("not unique name"));
510+
511+
// when
512+
simulateRunningTestCases(formatter, asList(testCase1, testCase2));
513+
514+
// then
515+
final InOrder inOrder = inOrder(instrumentation);
516+
assertThat(captureTestName(inOrder, instrumentation), equalTo("not unique name"));
517+
assertThat(captureTestName(inOrder, instrumentation), equalTo("not unique name"));
518+
}
519+
520+
@Test
521+
public void test_names_are_made_unique_also_when_not_consecutive() {
522+
523+
// given
524+
final AndroidInstrumentationReporter formatter = new AndroidInstrumentationReporter(runtime, instrumentation);
525+
TestCase testCase1 = mockTestCase(testCaseName("not unique name"));
526+
TestCase testCase2 = mockTestCase(testCaseName("unique name"));
527+
TestCase testCase3 = mockTestCase(testCaseName("not unique name"));
528+
529+
// when
530+
simulateRunningTestCases(formatter, asList(testCase1, testCase2, testCase3));
531+
532+
// then
533+
final InOrder inOrder = inOrder(instrumentation);
534+
assertThat(captureTestName(inOrder, instrumentation), equalTo("not unique name"));
535+
assertThat(captureTestName(inOrder, instrumentation), equalTo("unique name"));
536+
assertThat(captureTestName(inOrder, instrumentation), equalTo("not unique name 2"));
537+
}
538+
461539
private void mockResultStatus(Result result, Result.Type status) {
462540
when(result.getStatus()).thenReturn(status);
463541
when(result.is(Result.Type.PASSED)).thenReturn(status == Result.Type.PASSED);
464542
}
543+
544+
private TestCase mockTestCase(String testCaseName) {
545+
return mockTestCase(featureUri("path/file.feature"), testCaseName);
546+
}
547+
548+
private TestCase mockTestCase(String featureUri, String testCaseName) {
549+
TestCase testCase = mock(TestCase.class);
550+
when(testCase.getUri()).thenReturn(featureUri);
551+
when(testCase.getName()).thenReturn(testCaseName);
552+
return testCase;
553+
}
554+
555+
private String testCaseName(String name) {
556+
return name;
557+
}
558+
559+
private String featureUri(String uri) {
560+
return uri;
561+
}
562+
563+
private void simulateRunningTestCases(AndroidInstrumentationReporter formatter, List<TestCase> testCases) {
564+
mockResultStatus(firstResult, Result.Type.PASSED);
565+
formatter.setNumberOfTests(testCases.size());
566+
for (TestCase testCase : testCases) {
567+
formatter.startTestCase(testCase);
568+
formatter.finishTestStep(firstResult);
569+
formatter.finishTestCase();
570+
}
571+
}
572+
573+
private String captureTestName(InOrder inOrder, Instrumentation intrumentation) {
574+
final ArgumentCaptor<Bundle> captor = ArgumentCaptor.forClass(Bundle.class);
575+
inOrder.verify(instrumentation).sendStatus(eq(StatusCodes.START), captor.capture());
576+
final Bundle actualBundle = captor.getValue();
577+
return actualBundle.getString(AndroidInstrumentationReporter.StatusKeys.TEST);
578+
}
465579
}

core/src/main/java/cucumber/runtime/Utils.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import java.lang.reflect.TypeVariable;
99
import java.net.MalformedURLException;
1010
import java.net.URL;
11-
import java.util.ArrayList;
1211
import java.util.List;
1312
import java.util.Map;
1413

@@ -128,4 +127,13 @@ public static String htmlEscape(String s) {
128127
.replace("'", "&#x27;")
129128
.replace("/", "&#x2F;");
130129
}
130+
131+
public static String getUniqueTestNameForScenarioExample(String testCaseName, int exampleNumber) {
132+
return testCaseName + (includesBlank(testCaseName) ? " " : "_") + exampleNumber;
133+
}
134+
135+
private static boolean includesBlank(String testCaseName) {
136+
return testCaseName.indexOf(' ') != -1;
137+
}
138+
131139
}

core/src/main/java/cucumber/runtime/formatter/JUnitFormatter.java

+2-9
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@
1212
import cucumber.api.formatter.Formatter;
1313
import cucumber.api.formatter.StrictAware;
1414
import cucumber.runtime.CucumberException;
15+
import cucumber.runtime.Utils;
1516
import cucumber.runtime.io.URLOutputStream;
1617
import cucumber.runtime.io.UTF8OutputStreamWriter;
17-
import gherkin.GherkinDialect;
18-
import gherkin.GherkinDialectProvider;
1918
import org.w3c.dom.Document;
2019
import org.w3c.dom.Element;
2120
import org.w3c.dom.NodeList;
@@ -28,10 +27,8 @@
2827
import javax.xml.transform.TransformerFactory;
2928
import javax.xml.transform.dom.DOMSource;
3029
import javax.xml.transform.stream.StreamResult;
31-
3230
import java.io.Closeable;
3331
import java.io.IOException;
34-
import java.io.OutputStream;
3532
import java.io.PrintWriter;
3633
import java.io.StringWriter;
3734
import java.io.Writer;
@@ -233,18 +230,14 @@ private void writeElement(Document doc, Element tc) {
233230
private String calculateElementName(cucumber.api.TestCase testCase) {
234231
String testCaseName = testCase.getName();
235232
if (testCaseName.equals(previousTestCaseName)) {
236-
return testCaseName + (includesBlank(testCaseName) ? " " : "_") + ++exampleNumber;
233+
return Utils.getUniqueTestNameForScenarioExample(testCaseName, ++exampleNumber);
237234
} else {
238235
previousTestCaseName = testCase.getName();
239236
exampleNumber = 1;
240237
return testCaseName;
241238
}
242239
}
243240

244-
private boolean includesBlank(String testCaseName) {
245-
return testCaseName.indexOf(' ') != -1;
246-
}
247-
248241
public void addTestCaseElement(Document doc, Element tc, Result result) {
249242
tc.setAttribute("time", calculateTotalDurationString(result));
250243

examples/android/android-studio/Cukeulator/README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
## Cukeulator Example Test
2-
This is the example test-project for the Cukeulator app for Android Studio 1.0.2.
2+
This is the example test-project for the Cukeulator app for Android Studio 3.0+
33

44
### Setup
55
Features must be placed in `assets/features/`. Subdirectories are allowed.
@@ -67,3 +67,7 @@ adb shell am instrument -w cucumber.cukeulator.test/cucumber.cukeulator.test.Ins
6767

6868
### Output
6969
Filter for the logcat tag `cucumber-android` in [DDMS](https://developer.android.com/tools/debugging/ddms.html).
70+
71+
### Using this project with locally built Cucumber-JVM
72+
See [app/build.gradle](app/build.gradle) under `dependencies`.
73+
There is a source-code comment which explains how to use a locally built Cucumber-JVM Android library.

0 commit comments

Comments
 (0)