diff --git a/examples/jobserver/pom.xml b/examples/jobserver/pom.xml index c6688de1b..bc2c88053 100644 --- a/examples/jobserver/pom.xml +++ b/examples/jobserver/pom.xml @@ -11,7 +11,7 @@ UTF-8 1.8 3.6.0 - 0.9.6 + 2.0.0 diff --git a/karate-core/src/main/java/com/intuit/karate/Runner.java b/karate-core/src/main/java/com/intuit/karate/Runner.java index e5565ae7c..fd6a74d8f 100644 --- a/karate-core/src/main/java/com/intuit/karate/Runner.java +++ b/karate-core/src/main/java/com/intuit/karate/Runner.java @@ -35,7 +35,9 @@ import com.intuit.karate.core.HtmlFeatureReport; import com.intuit.karate.core.HtmlReport; import com.intuit.karate.core.HtmlSummaryReport; +import com.intuit.karate.core.ParallelProcessor; import com.intuit.karate.core.ScenarioExecutionUnit; +import com.intuit.karate.core.Subscriber; import com.intuit.karate.core.Tags; import com.intuit.karate.job.JobConfig; import com.intuit.karate.job.JobServer; @@ -45,9 +47,10 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -293,53 +296,79 @@ public static Results parallel(Builder options) { if (options.hooks != null) { options.hooks.forEach(h -> h.beforeAll(results)); } - ExecutorService featureExecutor = Executors.newFixedThreadPool(threadCount, Executors.privilegedThreadFactory()); - ExecutorService scenarioExecutor = Executors.newWorkStealingPool(threadCount); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + ExecutorService featureExecutor = Executors.newWorkStealingPool(threadCount); + List futures = new ArrayList(); + CompletableFuture latch = new CompletableFuture(); + Subscriber subscriber = new Subscriber() { + @Override + public void onNext(CompletableFuture result) { + futures.add(result); + } + + @Override + public void onComplete() { + latch.complete(Boolean.TRUE); + } + }; List resources = options.resolveResources(); - try { - int count = resources.size(); - CountDownLatch latch = new CountDownLatch(count); - List featureResults = new ArrayList(count); - for (int i = 0; i < count; i++) { - Resource resource = resources.get(i); - int index = i + 1; - Feature feature = FeatureParser.parse(resource); - feature.setCallName(options.scenarioName); - feature.setCallLine(resource.getLine()); - FeatureContext featureContext = new FeatureContext(null, feature, options.tagSelector()); - CallContext callContext = CallContext.forAsync(feature, options.hooks, options.hookFactory, null, false); - ExecutionContext execContext = new ExecutionContext(results, results.getStartTime(), featureContext, callContext, reportDir, - r -> featureExecutor.submit(r), scenarioExecutor, Thread.currentThread().getContextClassLoader()); - featureResults.add(execContext.result); - if (jobServer != null) { - List units = feature.getScenarioExecutionUnits(execContext); - jobServer.addFeature(execContext, units, () -> { - onFeatureDone(results, execContext, reportDir, index, count); - latch.countDown(); - }); - } else { - FeatureExecutionUnit unit = new FeatureExecutionUnit(execContext); - unit.setNext(() -> { - onFeatureDone(results, execContext, reportDir, index, count); - latch.countDown(); - }); - featureExecutor.submit(unit); + int count = resources.size(); + List featureResults = new ArrayList(count); + ParallelProcessor processor = new ParallelProcessor(featureExecutor, resources.iterator()) { + int index = 0; + + @Override + public Iterator process(Resource resource) { + CompletableFuture future = new CompletableFuture(); + try { + Feature feature = FeatureParser.parse(resource); + feature.setCallName(options.scenarioName); + feature.setCallLine(resource.getLine()); + FeatureContext featureContext = new FeatureContext(null, feature, options.tagSelector()); + CallContext callContext = CallContext.forAsync(feature, options.hooks, options.hookFactory, null, false); + ExecutionContext execContext = new ExecutionContext(results, results.getStartTime(), featureContext, callContext, reportDir, + r -> featureExecutor.submit(r), featureExecutor, classLoader); + featureResults.add(execContext.result); + if (jobServer != null) { + List units = feature.getScenarioExecutionUnits(execContext); + jobServer.addFeature(execContext, units, () -> { + onFeatureDone(results, execContext, reportDir, ++index, count); + future.complete(Boolean.TRUE); + }); + } else { + FeatureExecutionUnit unit = new FeatureExecutionUnit(execContext); + unit.setNext(() -> { + onFeatureDone(results, execContext, reportDir, ++index, count); + future.complete(Boolean.TRUE); + }); + unit.run(); + } + } catch (Exception e) { + future.complete(Boolean.FALSE); + LOGGER.error("runner failed: {}", e.getMessage()); + results.setFailureReason(e); } + return Collections.singletonList(future).iterator(); } + + }; + try { if (jobServer != null) { jobServer.startExecutors(); } - LOGGER.info("waiting for parallel features to complete ..."); + processor.subscribe(subscriber); + latch.join(); + CompletableFuture[] futuresArray = futures.toArray(new CompletableFuture[futures.size()]); + CompletableFuture allFutures = CompletableFuture.allOf(futuresArray); + LOGGER.info("waiting for {} parallel features to complete ...", futuresArray.length); if (options.timeoutMinutes > 0) { - latch.await(options.timeoutMinutes, TimeUnit.MINUTES); - if (latch.getCount() > 0) { - LOGGER.warn("parallel execution timed out after {} minutes, features remaining: {}", - options.timeoutMinutes, latch.getCount()); - } + allFutures.get(options.timeoutMinutes, TimeUnit.MINUTES); } else { - latch.await(); + allFutures.join(); } + LOGGER.info("all features complete"); results.stopTimer(); + featureExecutor.shutdownNow(); HtmlSummaryReport summary = new HtmlSummaryReport(); for (FeatureResult result : featureResults) { int scenarioCount = result.getScenarioCount(); @@ -367,11 +396,8 @@ public static Results parallel(Builder options) { options.hooks.forEach(h -> h.afterAll(results)); } } catch (Exception e) { - LOGGER.error("karate parallel runner failed: ", e.getMessage()); + LOGGER.error("runner failed: {}", e); results.setFailureReason(e); - } finally { - featureExecutor.shutdownNow(); - scenarioExecutor.shutdownNow(); } return results; } diff --git a/karate-core/src/main/java/com/intuit/karate/core/FeatureExecutionUnit.java b/karate-core/src/main/java/com/intuit/karate/core/FeatureExecutionUnit.java index e05be0ee1..a6bc122d7 100644 --- a/karate-core/src/main/java/com/intuit/karate/core/FeatureExecutionUnit.java +++ b/karate-core/src/main/java/com/intuit/karate/core/FeatureExecutionUnit.java @@ -24,10 +24,8 @@ package com.intuit.karate.core; import com.intuit.karate.Logger; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; +import java.util.Iterator; import org.slf4j.LoggerFactory; /** @@ -41,9 +39,7 @@ public class FeatureExecutionUnit implements Runnable { public final ExecutionContext exec; private final boolean parallelScenarios; - private CountDownLatch latch; - private List units; - private List results; + private Iterator units; private Runnable next; public FeatureExecutionUnit(ExecutionContext exec) { @@ -51,7 +47,7 @@ public FeatureExecutionUnit(ExecutionContext exec) { parallelScenarios = exec.scenarioExecutor != null; } - public List getScenarioExecutionUnits() { + public Iterator getScenarioExecutionUnits() { return units; } @@ -67,16 +63,13 @@ public void init() { hookResult = false; } if (hookResult == false) { - units = Collections.EMPTY_LIST; + units = Collections.emptyIterator(); } } } if (units == null) { // no hook failed - units = exec.featureContext.feature.getScenarioExecutionUnits(exec); + units = exec.featureContext.feature.getScenarioExecutionUnits(exec).iterator(); } - int count = units.size(); - results = new ArrayList(count); - latch = new CountDownLatch(count); } public void setNext(Runnable next) { @@ -90,30 +83,49 @@ public void run() { if (units == null) { init(); } - for (ScenarioExecutionUnit unit : units) { - if (isSelected(unit) && run(unit)) { - // unit.next should count down latch when done - } else { // un-selected / failed scenario - latch.countDown(); + Subscriber subscriber = new Subscriber() { + @Override + public void onNext(ScenarioResult result) { + exec.result.addResult(result); } - } - try { - latch.await(); - } catch (Exception e) { - throw new RuntimeException(e); - } - stop(); - if (next != null) { - next.run(); - } + + @Override + public void onComplete() { + stop(); + if (next != null) { + next.run(); + } + } + }; + ParallelProcessor processor = new ParallelProcessor(exec.scenarioExecutor, units) { + @Override + public Iterator process(ScenarioExecutionUnit unit) { + if (isSelected(unit) && !unit.result.isFailed()) { // can happen for dynamic scenario outlines with a failed background ! + unit.run(); + // we also hold a reference to the last scenario-context that executed + // for cases where the caller needs a result + lastContextExecuted = unit.getContext(); + return Collections.singletonList(unit.result).iterator(); + } else { + return Collections.emptyIterator(); + } + } + + @Override + public boolean runSync(ScenarioExecutionUnit unit) { + if (!parallelScenarios) { + return true; + } + Tags tags = unit.scenario.getTagsEffective(); + return tags.valuesFor("parallel").isAnyOf("false"); + } + + }; + processor.subscribe(subscriber); } + // extracted for junit 5 public void stop() { - // this is where the feature gets "populated" with stats - // but best of all, the original order is retained - for (ScenarioResult sr : results) { - exec.result.addResult(sr); - } if (lastContextExecuted != null) { // set result map that caller will see exec.result.setResultVars(lastContextExecuted.vars); @@ -175,28 +187,4 @@ public static boolean isSelected(FeatureContext fc, Scenario scenario, Logger lo return false; } - public boolean run(ScenarioExecutionUnit unit) { - // this is an elegant solution to retaining the order of scenarios - // in the final report - even if they run in parallel ! - results.add(unit.result); - if (unit.result.isFailed()) { // can happen for dynamic scenario outlines with a failed background ! - return false; - } - Tags tags = unit.scenario.getTagsEffective(); - unit.setNext(() -> { - latch.countDown(); - // we also hold a reference to the last scenario-context that executed - // for cases where the caller needs a result - lastContextExecuted = unit.getContext(); // IMPORTANT: will handle if actions is null - }); - boolean sequential = !parallelScenarios || tags.valuesFor("parallel").isAnyOf("false"); - // main - if (sequential) { - unit.run(); - } else { - exec.scenarioExecutor.submit(unit); - } - return true; - } - } diff --git a/karate-core/src/main/java/com/intuit/karate/core/ParallelProcessor.java b/karate-core/src/main/java/com/intuit/karate/core/ParallelProcessor.java new file mode 100644 index 000000000..c9ba1c58c --- /dev/null +++ b/karate-core/src/main/java/com/intuit/karate/core/ParallelProcessor.java @@ -0,0 +1,98 @@ +/* + * The MIT License + * + * Copyright 2020 Intuit Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.intuit.karate.core; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author pthomas3 + */ +public abstract class ParallelProcessor implements Processor { + + private static final Logger logger = LoggerFactory.getLogger(ParallelProcessor.class); + + private final ExecutorService executor; + private final Iterator publisher; + private final List> futures = new ArrayList(); + + private Subscriber subscriber; + + public ParallelProcessor(ExecutorService executor, Iterator publisher) { + this.executor = executor; + this.publisher = publisher; + } + + private void execute(I in) { + Iterator out = process(in); + out.forEachRemaining(o -> { + synchronized (this) { // synchronized is important if multiple threads + subscriber.onNext(o); + } + }); + } + + @Override + public void onNext(I in) { + if (runSync(in)) { + execute(in); + } else { + CompletableFuture cf = new CompletableFuture(); + futures.add(cf); + executor.submit(() -> { + execute(in); + cf.complete(Boolean.TRUE); + }); + } + } + + @Override + public void subscribe(Subscriber subscriber) { + this.subscriber = subscriber; + publisher.forEachRemaining(this::onNext); + CompletableFuture[] futuresArray = futures.toArray(new CompletableFuture[futures.size()]); + if (futuresArray.length > 0) { + executor.submit(() -> { // will not block caller even when waiting for completion + CompletableFuture.allOf(futuresArray).join(); + subscriber.onComplete(); + }); + } else { + subscriber.onComplete(); + } + } + + @Override + public abstract Iterator process(I in); + + public boolean runSync(I in) { + return false; + } + +} diff --git a/karate-core/src/main/java/com/intuit/karate/core/Processor.java b/karate-core/src/main/java/com/intuit/karate/core/Processor.java new file mode 100644 index 000000000..e69c03276 --- /dev/null +++ b/karate-core/src/main/java/com/intuit/karate/core/Processor.java @@ -0,0 +1,40 @@ +/* + * The MIT License + * + * Copyright 2020 Intuit Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.intuit.karate.core; + +import java.util.Iterator; + +/** + * + * @author pthomas3 + */ +public interface Processor { + + void onNext(I i); + + void subscribe(Subscriber s); + + Iterator process(I i); + +} diff --git a/karate-core/src/main/java/com/intuit/karate/core/ScenarioExecutionUnit.java b/karate-core/src/main/java/com/intuit/karate/core/ScenarioExecutionUnit.java index b77d8aebb..e006dbcc6 100644 --- a/karate-core/src/main/java/com/intuit/karate/core/ScenarioExecutionUnit.java +++ b/karate-core/src/main/java/com/intuit/karate/core/ScenarioExecutionUnit.java @@ -56,7 +56,6 @@ public class ScenarioExecutionUnit implements Runnable { private boolean stopped = false; private boolean aborted = false; private StepResult lastStepResult; - private Runnable next; private boolean last; private Step currentStep; @@ -121,10 +120,6 @@ public boolean isStopped() { return stopped; } - public void setNext(Runnable next) { - this.next = next; - } - public void setLast(boolean last) { this.last = last; } @@ -329,10 +324,6 @@ public void run() { } catch (Exception e) { result.addError("scenario execution failed", e); LOGGER.error("scenario execution failed: {}", e.getMessage()); - } finally { - if (next != null) { - next.run(); - } } } diff --git a/karate-core/src/main/java/com/intuit/karate/core/Subscriber.java b/karate-core/src/main/java/com/intuit/karate/core/Subscriber.java new file mode 100644 index 000000000..69428edab --- /dev/null +++ b/karate-core/src/main/java/com/intuit/karate/core/Subscriber.java @@ -0,0 +1,36 @@ +/* + * The MIT License + * + * Copyright 2020 Intuit Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.intuit.karate.core; + +/** + * + * @author pthomas3 + */ +public interface Subscriber { + + void onNext(T t); + + void onComplete(); + +} diff --git a/karate-demo/src/test/java/demo/upload/upload-retry.feature b/karate-demo/src/test/java/demo/upload/upload-retry.feature index c5dacb762..02c94868a 100644 --- a/karate-demo/src/test/java/demo/upload/upload-retry.feature +++ b/karate-demo/src/test/java/demo/upload/upload-retry.feature @@ -1,11 +1,13 @@ +@mock-servlet-todo Feature: file upload retry Background: * url demoBaseUrl Scenario: upload file - * def count = 0 - * def done = function(){ var temp = karate.get('count'); temp = temp + 1; karate.set('count', temp); return temp > 1 } + * def count = { value: 0 } + * configure retry = { interval: 100 } + * def done = function(){ return count.value++ == 1 } Given path 'files' And multipart file myFile = { read: 'test.pdf', filename: 'upload-name.pdf', contentType: 'application/pdf' } And multipart field message = 'hello world' diff --git a/karate-junit4/src/main/java/com/intuit/karate/junit4/FeatureInfo.java b/karate-junit4/src/main/java/com/intuit/karate/junit4/FeatureInfo.java index 2369bf9ce..68fd0ead2 100644 --- a/karate-junit4/src/main/java/com/intuit/karate/junit4/FeatureInfo.java +++ b/karate-junit4/src/main/java/com/intuit/karate/junit4/FeatureInfo.java @@ -34,7 +34,6 @@ import com.intuit.karate.core.PerfEvent; import com.intuit.karate.core.Scenario; import com.intuit.karate.core.ScenarioContext; -import com.intuit.karate.core.ScenarioExecutionUnit; import com.intuit.karate.core.ScenarioResult; import com.intuit.karate.core.Step; import com.intuit.karate.core.StepResult; @@ -50,7 +49,6 @@ public class FeatureInfo implements ExecutionHook { public final Feature feature; - public final ExecutionContext exec; public final Description description; public final FeatureExecutionUnit unit; @@ -69,13 +67,9 @@ public FeatureInfo(Feature feature, String tagSelector) { description = Description.createSuiteDescription(feature.getNameForReport(), feature.getResource().getPackageQualifiedName()); FeatureContext featureContext = new FeatureContext(null, feature, tagSelector); CallContext callContext = new CallContext(null, true, this); - exec = new ExecutionContext(null, System.currentTimeMillis(), featureContext, callContext, null, null, null); + ExecutionContext exec = new ExecutionContext(null, System.currentTimeMillis(), featureContext, callContext, null, null, null); unit = new FeatureExecutionUnit(exec); unit.init(); - for (ScenarioExecutionUnit u : unit.getScenarioExecutionUnits()) { - Description scenarioDescription = getScenarioDescription(u.scenario); - description.addChild(scenarioDescription); - } } @Override @@ -131,8 +125,8 @@ public boolean beforeStep(Step step, ScenarioContext context) { @Override public void afterStep(StepResult result, ScenarioContext context) { - } - + } + @Override public String getPerfEventName(HttpRequestBuilder req, ScenarioContext context) { diff --git a/karate-junit4/src/main/java/com/intuit/karate/junit4/Karate.java b/karate-junit4/src/main/java/com/intuit/karate/junit4/Karate.java index 6e8f03895..b0cee8fae 100644 --- a/karate-junit4/src/main/java/com/intuit/karate/junit4/Karate.java +++ b/karate-junit4/src/main/java/com/intuit/karate/junit4/Karate.java @@ -131,7 +131,7 @@ protected void runChild(Feature feature, RunNotifier notifier) { FeatureInfo info = featureMap.get(feature.getRelativePath()); info.setNotifier(notifier); info.unit.run(); - FeatureResult result = info.exec.result; + FeatureResult result = info.unit.exec.result; if (!result.isEmpty()) { result.printStats(null); HtmlFeatureReport.saveFeatureResult(targetDir, result); diff --git a/karate-junit5/src/main/java/com/intuit/karate/junit5/FeatureNode.java b/karate-junit5/src/main/java/com/intuit/karate/junit5/FeatureNode.java index 2e6d53767..13518dea0 100644 --- a/karate-junit5/src/main/java/com/intuit/karate/junit5/FeatureNode.java +++ b/karate-junit5/src/main/java/com/intuit/karate/junit5/FeatureNode.java @@ -32,9 +32,7 @@ import com.intuit.karate.core.HtmlFeatureReport; import com.intuit.karate.core.HtmlSummaryReport; import com.intuit.karate.core.ScenarioExecutionUnit; -import java.util.ArrayList; import java.util.Iterator; -import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DynamicTest; @@ -49,8 +47,7 @@ public class FeatureNode implements Iterator, Iterable public final FeatureExecutionUnit featureUnit; public final HtmlSummaryReport summary; public final String reportDir; - - Iterator iterator; + public final Iterator iterator; public FeatureNode(String reportDir, HtmlSummaryReport summary, Feature feature, String tagSelector) { this.reportDir = reportDir; @@ -61,16 +58,7 @@ public FeatureNode(String reportDir, HtmlSummaryReport summary, Feature feature, exec = new ExecutionContext(null, System.currentTimeMillis(), featureContext, callContext, null, null, null); featureUnit = new FeatureExecutionUnit(exec); featureUnit.init(); - List selected = new ArrayList(); - for (ScenarioExecutionUnit unit : featureUnit.getScenarioExecutionUnits()) { - if (featureUnit.isSelected(unit)) { // tag filtering - selected.add(unit); - } - } - if (!selected.isEmpty()) { // make sure we trigger junit html report on last unit (after tag filtering) - selected.get(selected.size() - 1).setLast(true); - } - iterator = selected.iterator(); + iterator = featureUnit.getScenarioExecutionUnits(); } @Override @@ -82,9 +70,11 @@ public boolean hasNext() { public DynamicTest next() { ScenarioExecutionUnit unit = iterator.next(); return DynamicTest.dynamicTest(unit.scenario.getNameForReport(), () -> { - featureUnit.run(unit); + if (featureUnit.isSelected(unit)) { + unit.run(); + } boolean failed = unit.result.isFailed(); - if (unit.isLast()) { + if (!iterator.hasNext()) { featureUnit.stop(); FeatureResult result = exec.result; if (!result.isEmpty()) {