diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 8610c0b5a..21a05e4b6 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -106,6 +106,28 @@ jobs: run: ./bin/allure --version - name: Generate demo report run: ./bin/allure generate testcmd-smoke-results + - name: Verify generated tree data + shell: pwsh + run: | + $requiredFiles = @( + 'allure-report/data/behaviors.json', + 'allure-report/data/behaviors.csv', + 'allure-report/data/packages.json', + 'allure-report/widgets/behaviors.json' + ) + + $missingFiles = $requiredFiles | Where-Object { + -not (Test-Path -LiteralPath $_ -PathType Leaf) + } + + if ($missingFiles.Count -gt 0) { + Write-Error "Missing generated report files: $($missingFiles -join ', ')" + exit 1 + } + + $requiredFiles | ForEach-Object { + Write-Host "Verified $_" + } report: needs: [build, testcmd] name: "Build report" diff --git a/allure-commandline/build.gradle.kts b/allure-commandline/build.gradle.kts index 0682b46b3..dcd43bcf8 100644 --- a/allure-commandline/build.gradle.kts +++ b/allure-commandline/build.gradle.kts @@ -52,12 +52,6 @@ tasks.distTar { val startScripts by tasks.existing(CreateStartScripts::class) { applicationName = "allure" classpath = files(tasks.jar) + configurations.runtimeClasspath.get() + files("src/lib/config") - doLast { - unixScript.writeText(unixScript.readText() - .replace(Regex("(?m)^APP_HOME="), "export APP_HOME=") - .replace("\$(uname)\" = \"Darwin", "") - ) - } } tasks.build { diff --git a/allure-commandline/src/main/java/io/qameta/allure/CommandLine.java b/allure-commandline/src/main/java/io/qameta/allure/CommandLine.java index b1c407659..129fba2a2 100644 --- a/allure-commandline/src/main/java/io/qameta/allure/CommandLine.java +++ b/allure-commandline/src/main/java/io/qameta/allure/CommandLine.java @@ -27,8 +27,14 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.ProviderNotFoundException; +import java.security.CodeSource; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -51,6 +57,10 @@ public class CommandLine { protected static final String GENERATE_COMMAND = "generate"; protected static final String OPEN_COMMAND = "open"; protected static final String PLUGIN_COMMAND = "plugin"; + private static final String CONFIG_DIRECTORY = "config"; + private static final String PLUGINS_DIRECTORY = "plugins"; + private static final String DEFAULT_CONFIG_FILE_NAME = "allure.yml"; + private static final String LIB_DIRECTORY = "lib"; private final MainCommand mainCommand; private final ServeCommand serveCommand; @@ -80,20 +90,76 @@ public CommandLine(final Commands commands) { } public static void main(final String[] args) throws InterruptedException { - final String allureHome = System.getenv("APP_HOME"); - final CommandLine commandLine; - if (Objects.isNull(allureHome)) { - commandLine = new CommandLine((Path) null); - LOGGER.info("APP_HOME is not set, using default configuration"); - } else { - commandLine = new CommandLine(Paths.get(allureHome)); + final Optional allureHome = resolveAllureHome(System.getenv("APP_HOME")); + if (!allureHome.isPresent()) { + LOGGER.info("Allure home is not set, using default configuration"); } + final CommandLine commandLine = new CommandLine(allureHome.orElse(null)); final ExitCode exitCode = commandLine .parse(args) .orElseGet(commandLine::run); System.exit(exitCode.getCode()); } + static Optional resolveAllureHome(final String allureHome, final URI codeSource) { + if (Objects.nonNull(allureHome)) { + return Optional.of(Paths.get(allureHome)); + } + return inferAllureHome(codeSource); + } + + private static Optional resolveAllureHome(final String allureHome) { + if (Objects.nonNull(allureHome)) { + return Optional.of(Paths.get(allureHome)); + } + return getCodeSource() + .flatMap(CommandLine::inferAllureHome); + } + + private static Optional getCodeSource() { + try { + return Optional.ofNullable(CommandLine.class.getProtectionDomain().getCodeSource()) + .map(CodeSource::getLocation) + .map(location -> { + try { + return location.toURI(); + } catch (URISyntaxException e) { + LOGGER.debug("Could not resolve commandline location", e); + return null; + } + }); + } catch (SecurityException e) { + LOGGER.debug("Could not access commandline location", e); + return Optional.empty(); + } + } + + private static Optional inferAllureHome(final URI codeSource) { + try { + final Path location = Paths.get(codeSource).toAbsolutePath().normalize(); + final Path lib = Files.isDirectory(location) ? location : location.getParent(); + if (Objects.isNull(lib) + || Objects.isNull(lib.getFileName()) + || !LIB_DIRECTORY.equals(lib.getFileName().toString())) { + return Optional.empty(); + } + final Path home = lib.getParent(); + return isAllureHome(home) + ? Optional.of(home) + : Optional.empty(); + } catch (IllegalArgumentException | FileSystemNotFoundException | ProviderNotFoundException + | SecurityException e) { + LOGGER.debug("Could not infer Allure home from commandline location {}", codeSource, e); + return Optional.empty(); + } + } + + private static boolean isAllureHome(final Path home) { + return Objects.nonNull(home) + && Files.isRegularFile(home.resolve(CONFIG_DIRECTORY).resolve(DEFAULT_CONFIG_FILE_NAME)) + && Files.isDirectory(home.resolve(PLUGINS_DIRECTORY)); + } + public Optional parse(final String... args) { if (args.length == 0) { printUsage(commander); diff --git a/allure-commandline/src/test/java/io/qameta/allure/CommandLineTest.java b/allure-commandline/src/test/java/io/qameta/allure/CommandLineTest.java index 6b9cfbdd8..0dd5ded10 100644 --- a/allure-commandline/src/test/java/io/qameta/allure/CommandLineTest.java +++ b/allure-commandline/src/test/java/io/qameta/allure/CommandLineTest.java @@ -26,6 +26,7 @@ import org.mockito.ArgumentCaptor; import java.io.IOException; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -62,6 +63,54 @@ void setUp() { this.commandLine = new CommandLine(commands); } + /** + * Verifies the explicit application home is used as the commandline home. + * The test checks APP_HOME has priority over the commandline location. + */ + @Description + @Test + void shouldResolveAllureHomeFromAppHome(@TempDir final Path temp) { + final Path appHome = temp.resolve("custom-home"); + final URI commandlineLocation = temp.resolve("allure").resolve("lib").resolve("allure-commandline.jar") + .toUri(); + + final Optional result = resolveAllureHome(appHome.toString(), commandlineLocation); + + assertThat(result) + .hasValue(appHome); + } + + /** + * Verifies the commandline can infer an installed distribution home. + * The test checks a commandline jar under lib resolves to the parent distribution directory. + */ + @Description + @Test + void shouldInferAllureHomeFromCommandlineJar(@TempDir final Path temp) throws IOException { + final Path allureHome = createAllureHome(temp); + final Path commandlineJar = createCommandlineJar(allureHome); + + final Optional result = resolveAllureHome(null, commandlineJar.toUri()); + + assertThat(result) + .hasValue(allureHome); + } + + /** + * Verifies invalid commandline locations are ignored. + * The test checks a candidate without distribution config and plugins does not resolve. + */ + @Description + @Test + void shouldIgnoreInvalidInferredAllureHome(@TempDir final Path temp) throws IOException { + final Path commandlineJar = createCommandlineJar(temp.resolve("invalid-allure")); + + final Optional result = resolveAllureHome(null, commandlineJar.toUri()); + + assertThat(result) + .isEmpty(); + } + /** * Verifies that invoking the parser without arguments is rejected. * The test checks the command line reports an argument parsing error. @@ -582,4 +631,55 @@ private String describeMainCommand() { verboseOptions.isQuiet() ); } + + @Step("Create installed Allure home") + private Path createAllureHome(final Path temp) throws IOException { + final Path allureHome = temp.resolve("allure"); + Files.createDirectories(allureHome.resolve("config")); + Files.write(allureHome.resolve("config").resolve("allure.yml"), Arrays.asList("plugins: []")); + Files.createDirectories(allureHome.resolve("plugins")); + Allure.addAttachment( + "allure-home-layout.txt", + "text/plain", + String.format( + "home=%s%nconfig=%s%nplugins=%s%n", + allureHome, + allureHome.resolve("config").resolve("allure.yml"), + allureHome.resolve("plugins") + ), + ".txt" + ); + return allureHome; + } + + @Step("Create commandline jar") + private Path createCommandlineJar(final Path allureHome) throws IOException { + final Path lib = Files.createDirectories(allureHome.resolve("lib")); + final Path commandlineJar = lib.resolve("allure-commandline-test.jar"); + Files.write(commandlineJar, Arrays.asList("commandline jar marker")); + Allure.addAttachment( + "commandline-location.txt", + "text/plain", + String.format("commandlineJar=%s%n", commandlineJar), + ".txt" + ); + return commandlineJar; + } + + @Step("Resolve Allure home") + private Optional resolveAllureHome(final String appHome, final URI commandlineLocation) { + final Optional result = CommandLine.resolveAllureHome(appHome, commandlineLocation); + Allure.addAttachment( + "allure-home-resolution.txt", + "text/plain", + String.format( + "appHome=%s%ncommandlineLocation=%s%nresolvedHome=%s%n", + appHome, + commandlineLocation, + result.map(Path::toString).orElse("") + ), + ".txt" + ); + return result; + } }