diff --git a/serenity-model/src/main/java/net/thucydides/model/requirements/RootDirectory.java b/serenity-model/src/main/java/net/thucydides/model/requirements/RootDirectory.java index fcba3fb988..3698e46d88 100644 --- a/serenity-model/src/main/java/net/thucydides/model/requirements/RootDirectory.java +++ b/serenity-model/src/main/java/net/thucydides/model/requirements/RootDirectory.java @@ -265,6 +265,23 @@ public Optional featuresOrStoriesRootDirectory() { URI serenityReqDir = new File(ThucydidesSystemProperty.SERENITY_REQUIREMENTS_DIR.from(environmentVariables)).toURI(); return Optional.of(Paths.get(serenityReqDir)); } + + // The Gradle plugin's `AggregateTask` wires its `requirementsBaseDir` DSL field + // into `serenity.test.requirements.basedir`, but does not mirror it into + // `serenity.requirements.dir`. For multi-module Gradle builds the working + // directory during aggregation is the root project (not the sub-module owning + // the feature files), so the classpath / resource scans below do not find the + // feature tree. Honouring the `basedir` property here makes the existing + // plugin wiring work without requiring callers to double-configure the same + // path under two property names. + if (ThucydidesSystemProperty.SERENITY_TEST_REQUIREMENTS_BASEDIR.isDefinedIn(environmentVariables)) { + File basedir = new File( + ThucydidesSystemProperty.SERENITY_TEST_REQUIREMENTS_BASEDIR.from(environmentVariables)); + if (basedir.isDirectory()) { + return Optional.of(basedir.toPath()); + } + } + List resourceDirectories = getResourceDirectories(Paths.get(relativeRoot), environmentVariables); List resourceDirectoriesByIncreasingDepth = resourceDirectories.stream() .sorted(Comparator.comparingInt(dir -> dir.getAbsolutePath().length())) diff --git a/serenity-model/src/test/java/net/thucydides/model/requirements/WhenFindingTheRootDirectoryForFeatureFiles.java b/serenity-model/src/test/java/net/thucydides/model/requirements/WhenFindingTheRootDirectoryForFeatureFiles.java index 6c080358bb..b608f9f40b 100644 --- a/serenity-model/src/test/java/net/thucydides/model/requirements/WhenFindingTheRootDirectoryForFeatureFiles.java +++ b/serenity-model/src/test/java/net/thucydides/model/requirements/WhenFindingTheRootDirectoryForFeatureFiles.java @@ -3,10 +3,17 @@ import net.thucydides.model.environment.MockEnvironmentVariables; import net.thucydides.model.util.EnvironmentVariables; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; import java.util.Set; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class WhenFindingTheRootDirectoryForFeatureFiles { EnvironmentVariables environmentVariables = new MockEnvironmentVariables(); @@ -24,4 +31,54 @@ void withADefinedProjectDirectory() { RootDirectory rootDirectory = new RootDirectory(environmentVariables,featureDirectory); Set directoryPaths = rootDirectory.getRootDirectoryPaths(); } + + @Test + void featuresOrStoriesRootDirectoryFallsBackToTestRequirementsBasedir(@TempDir Path tempDir) throws Exception { + // Simulates the Gradle plugin's AggregateTask wiring: `requirementsBaseDir` is set, + // but `serenity.requirements.dir` is not. featuresOrStoriesRootDirectory() should + // pick up the basedir instead of falling through to the working-directory scan + // (which misfires in multi-module Gradle builds because user.dir == root project). + Path featuresDir = tempDir.resolve("features"); + Files.createDirectories(featuresDir); + + environmentVariables.setProperty("serenity.test.requirements.basedir", featuresDir.toString()); + + RootDirectory rootDirectory = new RootDirectory(environmentVariables, "."); + Optional resolved = rootDirectory.featuresOrStoriesRootDirectory(); + + assertTrue(resolved.isPresent(), "Expected featuresOrStoriesRootDirectory() to honour serenity.test.requirements.basedir"); + assertEquals(featuresDir.toAbsolutePath(), resolved.get().toAbsolutePath()); + } + + @Test + void serenityRequirementsDirTakesPrecedenceOverBasedir(@TempDir Path tempDir) throws Exception { + // When both properties are set, the existing behaviour (SERENITY_REQUIREMENTS_DIR wins) + // must be preserved — the new fallback must not regress that order. + Path primary = tempDir.resolve("primary/features"); + Path fallback = tempDir.resolve("fallback/features"); + Files.createDirectories(primary); + Files.createDirectories(fallback); + + environmentVariables.setProperty("serenity.requirements.dir", primary.toString()); + environmentVariables.setProperty("serenity.test.requirements.basedir", fallback.toString()); + + Optional resolved = new RootDirectory(environmentVariables, ".").featuresOrStoriesRootDirectory(); + + assertTrue(resolved.isPresent()); + assertEquals(primary.toAbsolutePath(), resolved.get().toAbsolutePath()); + } + + @Test + void basedirFallbackIgnoresNonExistentPaths(@TempDir Path tempDir) { + // Guard against a stale basedir value masking the classpath/working-dir fallback. + String nonExistent = tempDir.resolve("does-not-exist").toString(); + environmentVariables.setProperty("serenity.test.requirements.basedir", nonExistent); + + Optional resolved = new RootDirectory(environmentVariables, ".").featuresOrStoriesRootDirectory(); + + // The fallback must not hand back a path that does not exist — callers treat the + // Optional.of(...) result as authoritative. + resolved.ifPresent(path -> assertTrue(path.toFile().exists(), + "featuresOrStoriesRootDirectory() should not return a non-existent basedir: " + path)); + } }