11package io .cucumber .scalatest
22
3- import io .cucumber .core .options .RuntimeOptionsBuilder
4- import io .cucumber .core .runtime .{Runtime => CucumberRuntime }
3+ import io .cucumber .core .feature .FeatureParser
4+ import io .cucumber .core .filter .Filters
5+ import io .cucumber .core .gherkin .{Feature , Pickle }
6+ import io .cucumber .core .options ._
7+ import io .cucumber .core .plugin .{PluginFactory , Plugins }
8+ import io .cucumber .core .runtime ._
59import org .scalatest .{Args , Status , Suite }
610
11+ import java .time .Clock
12+ import java .util .function .{Predicate , Supplier }
13+ import scala .jdk .CollectionConverters ._
714import scala .annotation .nowarn
815
916/** Configuration for Cucumber tests.
@@ -36,6 +43,9 @@ case class CucumberOptions(
3643 * - Environment variables starting with CUCUMBER_
3744 * - System properties starting with cucumber.
3845 *
46+ * Each feature file appears as a nested suite, and each scenario within a
47+ * feature appears as a test within that suite.
48+ *
3949 * Example:
4050 * {{{
4151 * import io.cucumber.scalatest.{CucumberOptions, CucumberSuite}
@@ -59,91 +69,99 @@ trait CucumberSuite extends Suite {
5969
6070 private lazy val classLoader : ClassLoader = getClass.getClassLoader
6171
62- /** Runs the Cucumber scenarios.
63- *
64- * @param testName
65- * An optional name of one test to run. If None, all relevant tests should
66- * be run.
67- * @param args
68- * the Args for this run
69- * @return
70- * a Status object that indicates when all tests started by this method
71- * have completed, and whether or not a failure occurred.
72- */
73- abstract override def run (
74- testName : Option [String ],
75- args : Args
76- ): Status = {
72+ private lazy val (features, context, filters) = {
73+ val runtimeOptions = buildRuntimeOptions()
74+ val classLoaderSupplier = new Supplier [ClassLoader ] {
75+ override def get (): ClassLoader = classLoader
76+ }
77+
78+ val uuidGeneratorServiceLoader =
79+ new UuidGeneratorServiceLoader (classLoaderSupplier, runtimeOptions)
80+ val bus = SynchronizedEventBus .synchronize(
81+ new TimeServiceEventBus (
82+ Clock .systemUTC(),
83+ uuidGeneratorServiceLoader.loadUuidGenerator()
84+ )
85+ )
86+
87+ val parser = new FeatureParser (bus.generateId _)
88+ val featureSupplier =
89+ new FeaturePathFeatureSupplier (
90+ classLoaderSupplier,
91+ runtimeOptions,
92+ parser
93+ )
94+ val features = featureSupplier.get().asScala.toList
95+
96+ val plugins = new Plugins (new PluginFactory (), runtimeOptions)
97+ val exitStatus = new ExitStatus (runtimeOptions)
98+ plugins.addPlugin(exitStatus)
99+
100+ val objectFactoryServiceLoader =
101+ new ObjectFactoryServiceLoader (classLoaderSupplier, runtimeOptions)
102+ val objectFactorySupplier =
103+ new ThreadLocalObjectFactorySupplier (objectFactoryServiceLoader)
104+ val backendSupplier =
105+ new BackendServiceLoader (classLoaderSupplier, objectFactorySupplier)
106+ val runnerSupplier = new ThreadLocalRunnerSupplier (
107+ runtimeOptions,
108+ bus,
109+ backendSupplier,
110+ objectFactorySupplier
111+ )
112+
113+ val context =
114+ new CucumberExecutionContext (bus, exitStatus, runnerSupplier)
115+ val filters : Predicate [Pickle ] = new Filters (runtimeOptions)
116+
117+ plugins.setEventBusOnEventListenerPlugins(bus)
118+
119+ (features, context, filters)
120+ }
121+
122+ override def nestedSuites : collection.immutable.IndexedSeq [Suite ] = {
123+ features
124+ .map(feature => new FeatureSuite (feature, context, filters))
125+ .toIndexedSeq
126+ }
127+
128+ override def run (testName : Option [String ], args : Args ): Status = {
77129 if (testName.isDefined) {
78130 throw new IllegalArgumentException (
79- " Suite traits implemented by Cucumber do not support running a single test "
131+ " Running a single test by name is not supported in CucumberSuite "
80132 )
81133 }
134+ context.runFeatures(() => super .run(testName, args))
135+ org.scalatest.SucceededStatus
136+ }
82137
83- val runtimeOptions = buildRuntimeOptions()
84- val classLoader = getClass.getClassLoader
138+ private def buildRuntimeOptions () : RuntimeOptions = {
139+ val packageName = getClass.getPackage.getName
85140
86- val runtime = CucumberRuntime
87- .builder()
88- .withRuntimeOptions(runtimeOptions)
89- .withClassLoader(new java.util.function.Supplier [ClassLoader ] {
90- override def get (): ClassLoader = classLoader
91- })
141+ // Parse options from different sources in order of precedence
142+ val propertiesFileOptions = new CucumberPropertiesParser ()
143+ .parse(CucumberProperties .fromPropertiesFile())
92144 .build()
93145
94- runtime.run( )
146+ val annotationOptions = buildProgrammaticOptions(propertiesFileOptions )
95147
96- val exitStatus = runtime.exitStatus()
97- if (exitStatus == 0 ) {
98- org.scalatest.SucceededStatus
99- } else {
100- throw new RuntimeException (
101- s " Cucumber scenarios failed with exit status: $exitStatus"
102- )
103- }
148+ val environmentOptions = new CucumberPropertiesParser ()
149+ .parse(CucumberProperties .fromEnvironment())
150+ .build(annotationOptions)
151+
152+ val runtimeOptions = new CucumberPropertiesParser ()
153+ .parse(CucumberProperties .fromSystemProperties())
154+ .build(environmentOptions)
155+
156+ runtimeOptions
104157 }
105158
106- private def buildRuntimeOptions (): io.cucumber.core.options.RuntimeOptions = {
159+ private def buildProgrammaticOptions (
160+ base : RuntimeOptions
161+ ): RuntimeOptions = {
107162 val packageName = getClass.getPackage.getName
108163 val builder = new RuntimeOptionsBuilder ()
109164
110- // Parse options from cucumber.properties file on classpath
111- scala.util.Try {
112- val propertiesUrl = classLoader.getResource(" cucumber.properties" )
113- if (propertiesUrl != null ) {
114- val props = new java.util.Properties ()
115- val is = propertiesUrl.openStream()
116- try {
117- props.load(is)
118- } finally {
119- is.close()
120- }
121- import scala .jdk .CollectionConverters ._
122- props.asScala.foreach { case (key, value) =>
123- applyProperty(builder, key.toString, value.toString)
124- }
125- }
126- }
127-
128- // Parse options from environment variables (CUCUMBER_*)
129- scala.util.Try {
130- sys.env.foreach { case (key, value) =>
131- if (key.startsWith(" CUCUMBER_" )) {
132- val propertyName = key.substring(9 ).toLowerCase.replace('_' , '.' )
133- applyProperty(builder, " cucumber." + propertyName, value)
134- }
135- }
136- }
137-
138- // Parse options from system properties (cucumber.*)
139- scala.util.Try {
140- sys.props.foreach { case (key, value) =>
141- if (key.startsWith(" cucumber." )) {
142- applyProperty(builder, key, value)
143- }
144- }
145- }
146-
147165 // Add features (programmatic options take precedence)
148166 val features =
149167 if (cucumberOptions.features.nonEmpty) cucumberOptions.features
@@ -176,35 +194,62 @@ trait CucumberSuite extends Suite {
176194 )
177195 }
178196
179- builder.build()
197+ builder.build(base )
180198 }
181199
182- private def applyProperty (
183- builder : RuntimeOptionsBuilder ,
184- key : String ,
185- value : String
186- ): Unit = {
187- // Map property keys to builder methods
188- key match {
189- case " cucumber.glue" =>
190- value.split(" ," ).foreach { g =>
191- builder.addGlue(java.net.URI .create(" classpath:" + g.trim))
192- }
193- case " cucumber.plugin" =>
194- value.split(" ," ).foreach { p =>
195- builder.addPluginName(p.trim)
196- }
197- case " cucumber.tags" | " cucumber.filter.tags" =>
198- builder.addTagFilter(
199- io.cucumber.tagexpressions.TagExpressionParser .parse(value)
200- )
201- case " cucumber.features" =>
202- value.split(" ," ).foreach { f =>
203- builder.addFeature(
204- io.cucumber.core.feature.FeatureWithLines .parse(f.trim)
205- )
200+ private class FeatureSuite (
201+ feature : Feature ,
202+ context : CucumberExecutionContext ,
203+ filters : Predicate [Pickle ]
204+ ) extends Suite {
205+
206+ override def suiteName : String =
207+ feature.getName.orElse(" EMPTY_NAME" )
208+
209+ override def nestedSuites : collection.immutable.IndexedSeq [Suite ] = {
210+ feature
211+ .getPickles()
212+ .asScala
213+ .filter(filters.test)
214+ .map(pickle => new PickleSuite (feature, pickle, context))
215+ .toIndexedSeq
216+ }
217+
218+ override def run (testName : Option [String ], args : Args ): Status = {
219+ context.beforeFeature(feature)
220+ super .run(testName, args)
221+ }
222+ }
223+
224+ private class PickleSuite (
225+ feature : Feature ,
226+ pickle : Pickle ,
227+ context : CucumberExecutionContext
228+ ) extends Suite {
229+
230+ override def suiteName : String = pickle.getName
231+
232+ override def testNames : Set [String ] = Set (pickle.getName)
233+
234+ override protected def runTest (
235+ testName : String ,
236+ args : Args
237+ ): Status = {
238+ var testFailed : Option [Throwable ] = None
239+
240+ context.runTestCase(runner => {
241+ try {
242+ runner.runPickle(pickle)
243+ } catch {
244+ case ex : Throwable =>
245+ testFailed = Some (ex)
206246 }
207- case _ => // Ignore unknown properties
247+ })
248+
249+ testFailed match {
250+ case Some (ex) => throw ex
251+ case None => org.scalatest.SucceededStatus
252+ }
208253 }
209254 }
210255}
0 commit comments