diff --git a/core/src/main/java/cucumber/api/StepDefinitionReporter.java b/core/src/main/java/cucumber/api/StepDefinitionReporter.java index 81c317c57f..6a901eedb8 100644 --- a/core/src/main/java/cucumber/api/StepDefinitionReporter.java +++ b/core/src/main/java/cucumber/api/StepDefinitionReporter.java @@ -1,7 +1,12 @@ package cucumber.api; +import cucumber.api.event.StepDefinedEvent; import cucumber.runtime.StepDefinition; +/** + * @deprecated in favor of {@link StepDefinedEvent}, as Lambda Step Definitions are not reported through this class. + */ +@Deprecated public interface StepDefinitionReporter extends Plugin { /** * Called when a step definition is defined diff --git a/core/src/main/java/cucumber/api/event/CanonicalEventOrder.java b/core/src/main/java/cucumber/api/event/CanonicalEventOrder.java index 9236abb98d..240fcf3d8c 100644 --- a/core/src/main/java/cucumber/api/event/CanonicalEventOrder.java +++ b/core/src/main/java/cucumber/api/event/CanonicalEventOrder.java @@ -31,6 +31,7 @@ private static final class FixedEventOrderComparator implements Comparator{@link TestRunStarted} - the first event sent. *
  • {@link TestSourceRead} - sent for each feature file read, contains the feature file source. *
  • {@link SnippetsSuggestedEvent} - sent for each step that could not be matched to a step definition, contains the raw snippets for the step. + *
  • {@link StepDefinedEvent} - sent for each step definition as it is loaded, contains the StepDefinition *
  • {@link TestCaseStarted} - sent before starting the execution of a Test Case(/Pickle/Scenario), contains the Test Case *
  • {@link TestStepStarted} - sent before starting the execution of a Test Step, contains the Test Step *
  • {@link EmbedEvent} - calling scenario.embed in a hook triggers this event. diff --git a/core/src/main/java/cucumber/api/event/StepDefinedEvent.java b/core/src/main/java/cucumber/api/event/StepDefinedEvent.java new file mode 100644 index 0000000000..245f68eff4 --- /dev/null +++ b/core/src/main/java/cucumber/api/event/StepDefinedEvent.java @@ -0,0 +1,13 @@ +package cucumber.api.event; + +import cucumber.runtime.StepDefinition; + +public class StepDefinedEvent extends TimeStampedEvent { + public final StepDefinition stepDefinition; + + public StepDefinedEvent(Long time, Long timeMillis, StepDefinition stepDefinition) { + super(time, timeMillis); + this.stepDefinition = stepDefinition; + } + +} diff --git a/core/src/main/java/cucumber/runner/Glue.java b/core/src/main/java/cucumber/runner/Glue.java index 21e1fa97e6..dc4de3b949 100644 --- a/core/src/main/java/cucumber/runner/Glue.java +++ b/core/src/main/java/cucumber/runner/Glue.java @@ -2,9 +2,11 @@ import cucumber.runtime.DuplicateStepDefinitionException; import cucumber.runtime.HookDefinition; +import cucumber.runtime.ScenarioScoped; import cucumber.runtime.StepDefinition; import io.cucumber.stepexpression.Argument; import cucumber.api.StepDefinitionReporter; +import cucumber.api.event.StepDefinedEvent; import gherkin.pickles.PickleStep; import java.util.ArrayList; @@ -23,6 +25,12 @@ final class Glue implements cucumber.runtime.Glue { final List afterHooks = new ArrayList<>(); final List afterStepHooks = new ArrayList<>(); + private final EventBus bus; + + public Glue(EventBus bus) { + this.bus = bus; + } + @Override public void addStepDefinition(StepDefinition stepDefinition) { StepDefinition previous = stepDefinitionsByPattern.get(stepDefinition.getPattern()); @@ -30,6 +38,7 @@ public void addStepDefinition(StepDefinition stepDefinition) { throw new DuplicateStepDefinitionException(previous, stepDefinition); } stepDefinitionsByPattern.put(stepDefinition.getPattern(), stepDefinition); + bus.send(new StepDefinedEvent(bus.getTime(), bus.getTimeMillis(), stepDefinition)); } @Override @@ -130,6 +139,10 @@ private void removeScenarioScopedHooks(List beforeHooks) { Iterator hookIterator = beforeHooks.iterator(); while (hookIterator.hasNext()) { HookDefinition hook = hookIterator.next(); + if (hook instanceof ScenarioScoped) { + ScenarioScoped scenarioScopedHookDefinition = (ScenarioScoped) hook; + scenarioScopedHookDefinition.disposeScenarioScope(); + } if (hook.isScenarioScoped()) { hookIterator.remove(); } @@ -140,6 +153,10 @@ private void removeScenariosScopedStepDefinitions(Map st Iterator> stepDefinitionIterator = stepDefinitions.entrySet().iterator(); while(stepDefinitionIterator.hasNext()){ StepDefinition stepDefinition = stepDefinitionIterator.next().getValue(); + if (stepDefinition instanceof ScenarioScoped) { + ScenarioScoped scenarioScopedStepDefinition = (ScenarioScoped) stepDefinition; + scenarioScopedStepDefinition.disposeScenarioScope(); + } if(stepDefinition.isScenarioScoped()){ stepDefinitionIterator.remove(); } diff --git a/core/src/main/java/cucumber/runner/Runner.java b/core/src/main/java/cucumber/runner/Runner.java index e55841146e..62633fceb2 100644 --- a/core/src/main/java/cucumber/runner/Runner.java +++ b/core/src/main/java/cucumber/runner/Runner.java @@ -6,12 +6,12 @@ import cucumber.runtime.Backend; import cucumber.runtime.HookDefinition; import cucumber.util.FixJava; -import io.cucumber.core.logging.Logger; -import io.cucumber.core.logging.LoggerFactory; -import io.cucumber.core.options.RunnerOptions; import gherkin.events.PickleEvent; import gherkin.pickles.PickleStep; import gherkin.pickles.PickleTag; +import io.cucumber.core.logging.Logger; +import io.cucumber.core.logging.LoggerFactory; +import io.cucumber.core.options.RunnerOptions; import java.net.URI; import java.util.ArrayList; @@ -22,13 +22,14 @@ public final class Runner { private static final Logger log = LoggerFactory.getLogger(Runner.class); - private final Glue glue = new Glue(); + private final Glue glue; private final EventBus bus; private final Collection backends; private final RunnerOptions runnerOptions; public Runner(EventBus bus, Collection backends, RunnerOptions runnerOptions) { this.bus = bus; + this.glue = new Glue(bus); this.runnerOptions = runnerOptions; this.backends = backends; List gluePaths = runnerOptions.getGlue(); @@ -134,5 +135,6 @@ private void disposeBackendWorlds() { for (Backend backend : backends) { backend.disposeWorld(); } + glue.removeScenarioScopedGlue(); } } diff --git a/core/src/main/java/cucumber/runtime/HookDefinition.java b/core/src/main/java/cucumber/runtime/HookDefinition.java index 02683bb746..4e6596cc1b 100644 --- a/core/src/main/java/cucumber/runtime/HookDefinition.java +++ b/core/src/main/java/cucumber/runtime/HookDefinition.java @@ -21,7 +21,9 @@ public interface HookDefinition { int getOrder(); /** + * @deprecated replaced with {@link ScenarioScoped} * @return true if this instance is scoped to a single scenario, or false if it can be reused across scenarios. */ + @Deprecated boolean isScenarioScoped(); } diff --git a/core/src/main/java/cucumber/runtime/ScenarioScoped.java b/core/src/main/java/cucumber/runtime/ScenarioScoped.java new file mode 100644 index 0000000000..fd8b80e0ab --- /dev/null +++ b/core/src/main/java/cucumber/runtime/ScenarioScoped.java @@ -0,0 +1,8 @@ +package cucumber.runtime; + +public interface ScenarioScoped { + /** + * Dispose references to Runtime world to allow garbage collection to run. + */ + void disposeScenarioScope(); +} diff --git a/core/src/main/java/cucumber/runtime/StepDefinition.java b/core/src/main/java/cucumber/runtime/StepDefinition.java index f2aeb19237..82d3c48d9a 100644 --- a/core/src/main/java/cucumber/runtime/StepDefinition.java +++ b/core/src/main/java/cucumber/runtime/StepDefinition.java @@ -44,7 +44,9 @@ public interface StepDefinition { String getPattern(); /** + * @deprecated replaced with {@link ScenarioScoped} * @return true if this instance is scoped to a single scenario, or false if it can be reused across scenarios. */ + @Deprecated boolean isScenarioScoped(); } diff --git a/core/src/test/java/cucumber/runner/GlueTest.java b/core/src/test/java/cucumber/runner/GlueTest.java index 62f4f23aeb..106ac97137 100644 --- a/core/src/test/java/cucumber/runner/GlueTest.java +++ b/core/src/test/java/cucumber/runner/GlueTest.java @@ -41,12 +41,12 @@ public class GlueTest { @Before public void setUp() { - glue = new Glue(); + glue = new Glue(mock(EventBus.class)); } @Test public void throws_duplicate_error_on_dupe_stepdefs() { - Glue glue = new Glue(); + Glue glue = new Glue(mock(EventBus.class)); StepDefinition a = mock(StepDefinition.class); when(a.getPattern()).thenReturn("hello"); diff --git a/core/src/test/java/cucumber/runtime/RuntimeTest.java b/core/src/test/java/cucumber/runtime/RuntimeTest.java index 22c1475eee..c2424daa6c 100644 --- a/core/src/test/java/cucumber/runtime/RuntimeTest.java +++ b/core/src/test/java/cucumber/runtime/RuntimeTest.java @@ -6,11 +6,8 @@ import cucumber.api.Scenario; import cucumber.api.StepDefinitionReporter; import cucumber.api.TestCase; -import cucumber.api.event.ConcurrentEventListener; -import cucumber.api.event.EventHandler; -import cucumber.api.event.EventPublisher; -import cucumber.api.event.TestCaseFinished; -import cucumber.api.event.TestStepFinished; +import cucumber.api.event.*; +import cucumber.api.event.EventListener; import cucumber.runner.EventBus; import cucumber.runner.TestBackendSupplier; import cucumber.runner.TestHelper; @@ -25,24 +22,18 @@ import cucumber.runtime.model.CucumberFeature; import gherkin.ast.ScenarioDefinition; import gherkin.ast.Step; +import gherkin.pickles.PickleStep; import gherkin.pickles.PickleTag; +import io.cucumber.stepexpression.Argument; import io.cucumber.stepexpression.TypeRegistry; import org.junit.Rule; import org.junit.Test; -import org.junit.internal.matchers.ThrowableMessageMatcher; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import java.net.URI; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.TimeUnit; +import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import static cucumber.runner.TestHelper.feature; @@ -52,11 +43,7 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage; +import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -85,10 +72,10 @@ public void runs_feature_with_json_formatter() { Plugin jsonFormatter = FormatterBuilder.jsonFormatter(out); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - BackendSupplier backendSupplier = new BackendSupplier() { + BackendSupplier backendSupplier = new TestBackendSupplier() { @Override - public Collection get() { - return singletonList(mock(Backend.class)); + public void loadGlue(Glue glue, List gluePaths) { + } }; FeatureSupplier featureSupplier = new FeatureSupplier() { @@ -557,6 +544,83 @@ public void run() { assertTrue(interruptHit.get()); } + @Test + public void generates_events_for_glue_and_scenario_scoped_glue() { + final CucumberFeature feature = feature("test.feature", "" + + "Feature: feature name\n" + + " Scenario: Run a scenario once\n" + + " Given global scoped\n" + + " And scenario scoped\n" + + " Scenario: Then do it again\n" + + " Given global scoped\n" + + " And scenario scoped\n" + + ""); + + final List stepDefinedEvents = new ArrayList<>(); + + Plugin eventListener = new EventListener() { + @Override + public void setEventPublisher(EventPublisher publisher) { + publisher.registerHandlerFor(StepDefinedEvent.class, new EventHandler() { + @Override + public void receive(StepDefinedEvent event) { + stepDefinedEvents.add(event.stepDefinition); + } + }); + } + }; + + + final List definedStepDefinitions = new ArrayList<>(); + + BackendSupplier backendSupplier = new TestBackendSupplier() { + + private Glue glue; + + @Override + public void loadGlue(Glue glue, List gluePaths) { + this.glue = glue; + final StepDefinition mockedStepDefinition = new MockedStepDefinition(); + definedStepDefinitions.add(mockedStepDefinition); + glue.addStepDefinition(mockedStepDefinition); + } + + @Override + public void buildWorld() { + final StepDefinition mockedScenarioScopedStepDefinition = new MockedScenarioScopedStepDefinition(); + definedStepDefinitions.add(mockedScenarioScopedStepDefinition); + glue.addStepDefinition(mockedScenarioScopedStepDefinition); + } + }; + + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + FeatureSupplier featureSupplier = new FeatureSupplier() { + @Override + public List get() { + return singletonList(feature); + } + }; + Runtime.builder() + .withBackendSupplier(backendSupplier) + .withAdditionalPlugins(eventListener) + .withResourceLoader(new ClasspathResourceLoader(classLoader)) + .withEventBus(new TimeServiceEventBus(new TimeServiceStub(0))) + .withFeatureSupplier(featureSupplier) + .build() + .run(); + + assertThat(stepDefinedEvents, equalTo(definedStepDefinitions)); + + for (StepDefinition stepDefinedEvent : stepDefinedEvents) { + if(stepDefinedEvent instanceof MockedScenarioScopedStepDefinition){ + MockedScenarioScopedStepDefinition mocked = (MockedScenarioScopedStepDefinition) stepDefinedEvent; + assertTrue("Scenario scoped step definition should be disposed of", mocked.disposed); + } + } + + } + private String runFeatureWithFormatterSpy(CucumberFeature feature, Map stepsToResult) { FormatterSpy formatterSpy = new FormatterSpy(); @@ -596,11 +660,10 @@ private Runtime createRuntime(String... runtimeArgs) { } private Runtime createRuntime(ResourceLoader resourceLoader, ClassLoader classLoader, String... runtimeArgs) { - BackendSupplier backendSupplier = new BackendSupplier() { + BackendSupplier backendSupplier = new TestBackendSupplier(){ @Override - public Collection get() { - Backend backend = mock(Backend.class); - return singletonList(backend); + public void loadGlue(Glue glue, List gluePaths) { + } }; @@ -668,4 +731,87 @@ private void mockHook(Glue glue, HookDefinition hook, HookType hookType) { private TestCaseFinished testCaseFinishedWithStatus(Result.Type resultStatus) { return new TestCaseFinished(ANY_TIMESTAMP, ANY_TIMESTAMP, mock(TestCase.class), new Result(resultStatus, 0L, null)); } + + private static final class MockedStepDefinition implements StepDefinition { + + @Override + public List matchedArguments(PickleStep step) { + return step.getText().equals(getPattern()) ? new ArrayList() : null; + } + + @Override + public String getLocation(boolean detail) { + return "mocked step definition"; + } + + @Override + public Integer getParameterCount() { + return 0; + } + + @Override + public void execute(Object[] args) throws Throwable { + + } + + @Override + public boolean isDefinedAt(StackTraceElement stackTraceElement) { + return false; + } + + @Override + public String getPattern() { + return "global scoped"; + } + + @Override + public boolean isScenarioScoped() { + return true; + } + } + + private static final class MockedScenarioScopedStepDefinition implements StepDefinition, ScenarioScoped { + + boolean disposed; + + @Override + public void disposeScenarioScope() { + this.disposed = true; + } + + @Override + public List matchedArguments(PickleStep step) { + return step.getText().equals(getPattern()) ? new ArrayList() : null; + } + + @Override + public String getLocation(boolean detail) { + return "mocked scenario scoped step definition"; + } + + @Override + public Integer getParameterCount() { + return 0; + } + + @Override + public void execute(Object[] args) { + + } + + @Override + public boolean isDefinedAt(StackTraceElement stackTraceElement) { + return false; + } + + @Override + public String getPattern() { + return "scenario scoped"; + } + + @Override + public boolean isScenarioScoped() { + return true; + } + } } diff --git a/java/src/main/java/cucumber/runtime/java/JavaBackend.java b/java/src/main/java/cucumber/runtime/java/JavaBackend.java index 225a2fdaf5..8e44263144 100644 --- a/java/src/main/java/cucumber/runtime/java/JavaBackend.java +++ b/java/src/main/java/cucumber/runtime/java/JavaBackend.java @@ -117,7 +117,6 @@ public void buildWorld() { // in the constructor. try { INSTANCE.set(this); - glue.removeScenarioScopedGlue(); for (Class glueBaseClass : glueBaseClasses) { objectFactory.getInstance(glueBaseClass); } diff --git a/java8/src/main/java/cucumber/runtime/java8/Java8HookDefinition.java b/java8/src/main/java/cucumber/runtime/java8/Java8HookDefinition.java index 04dab0e791..0e513b69cb 100644 --- a/java8/src/main/java/cucumber/runtime/java8/Java8HookDefinition.java +++ b/java8/src/main/java/cucumber/runtime/java8/Java8HookDefinition.java @@ -6,18 +6,19 @@ import cucumber.api.java8.HookBody; import cucumber.api.java8.HookNoArgsBody; import cucumber.runtime.HookDefinition; +import cucumber.runtime.ScenarioScoped; import cucumber.runtime.filter.TagPredicate; import cucumber.runtime.Timeout; import gherkin.pickles.PickleTag; import java.util.Collection; -public class Java8HookDefinition implements HookDefinition { +public class Java8HookDefinition implements HookDefinition, ScenarioScoped { private final TagPredicate tagPredicate; private final int order; private final long timeoutMillis; private final HookNoArgsBody hookNoArgsBody; - private final HookBody hookBody; + private HookBody hookBody; private final StackTraceElement location; private Java8HookDefinition(String[] tagExpressions, int order, long timeoutMillis, HookBody hookBody, HookNoArgsBody hookNoArgsBody) { @@ -69,4 +70,9 @@ public int getOrder() { public boolean isScenarioScoped() { return true; } + + @Override + public void disposeScenarioScope() { + this.hookBody = null; + } } diff --git a/java8/src/main/java/cucumber/runtime/java8/Java8StepDefinition.java b/java8/src/main/java/cucumber/runtime/java8/Java8StepDefinition.java index 1324d4d3c9..8ae81887fc 100644 --- a/java8/src/main/java/cucumber/runtime/java8/Java8StepDefinition.java +++ b/java8/src/main/java/cucumber/runtime/java8/Java8StepDefinition.java @@ -9,6 +9,7 @@ import cucumber.api.java8.StepdefBody; import io.cucumber.stepexpression.ArgumentMatcher; import cucumber.runtime.CucumberException; +import cucumber.runtime.ScenarioScoped; import io.cucumber.stepexpression.ExpressionArgumentMatcher; import cucumber.runtime.StepDefinition; import io.cucumber.stepexpression.StepExpression; @@ -24,7 +25,7 @@ import java.util.List; import java.util.Map; -public class Java8StepDefinition implements StepDefinition { +public class Java8StepDefinition implements StepDefinition, ScenarioScoped { public static Java8StepDefinition create( String expression, Class bodyClass, T body, TypeRegistry typeRegistry) { @@ -37,7 +38,7 @@ public static StepDefinition create( } private final long timeoutMillis; - private final StepdefBody body; + private StepdefBody body; private final StepExpression expression; private final StackTraceElement location; @@ -161,4 +162,9 @@ private Type requireNonMapOrListType(Type argumentType) { return argumentType; } } + + @Override + public void disposeScenarioScope() { + this.body = null; + } }