> sortedEntryStream() {
- return environment.entrySet().stream().sorted(Map.Entry.comparingByKey());
- }
-
- /**
- * Returns a string that can be appended to /etc/environment.
- */
- public String getEtcEnvironmentString() {
- return sortedEntryStream().map(entry -> entry.getKey() + "=" + quoteIfNecessary(entry.getValue()))
- .collect(Collectors.joining("\n"));
- }
-
- /**
- * Returns a string that can be used in the DefaultEnvironment setting of a global systemd config file.
- */
- public String getSystemdString() {
- return sortedEntryStream().map(entry -> quoteIfNecessary(entry.getKey() + "=" + entry.getValue()))
- .collect(Collectors.joining(" "));
- }
-
-}
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/FatalInstallerError.java b/installer/src/main/java/com/teamscale/profiler/installer/FatalInstallerError.java
index 3d758fcf6..26c34d0a0 100644
--- a/installer/src/main/java/com/teamscale/profiler/installer/FatalInstallerError.java
+++ b/installer/src/main/java/com/teamscale/profiler/installer/FatalInstallerError.java
@@ -12,16 +12,14 @@ public FatalInstallerError(String message, Throwable throwable) {
}
/**
- * Prints this error to stderr. The stack trace of this exception is suppressed, but stack traces of any cause are
- * printed.
- *
- * we suppress stack traces of FatalInstallerErrors since these are errors we handled explicitly and the message
- * itself is supposed to be clear.
+ * Prints this error to stderr. All stack traces of this exception are suppressed since these are errors we handled
+ * explicitly and the message itself is supposed to be clear.
+ * If the error has a cause, its message is printed.
*/
public void printToStderr() {
System.err.println(getMessage());
if (getCause() != null) {
- getCause().printStackTrace(System.err);
+ System.err.println(getCause().getMessage());
}
}
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/Installer.java b/installer/src/main/java/com/teamscale/profiler/installer/Installer.java
index ce48ffc8b..c285decf1 100644
--- a/installer/src/main/java/com/teamscale/profiler/installer/Installer.java
+++ b/installer/src/main/java/com/teamscale/profiler/installer/Installer.java
@@ -4,43 +4,70 @@
import com.teamscale.profiler.installer.steps.InstallAgentFilesStep;
import com.teamscale.profiler.installer.steps.InstallEtcEnvironmentStep;
import com.teamscale.profiler.installer.steps.InstallSystemdStep;
-import org.conqat.lib.commons.collections.CollectionUtils;
-import org.conqat.lib.commons.system.SystemUtils;
+import com.teamscale.profiler.installer.steps.InstallWindowsSystemEnvironmentStep;
+import com.teamscale.profiler.installer.utils.TeamscaleUtils;
+import com.teamscale.profiler.installer.windows.IRegistry;
+import com.teamscale.profiler.installer.windows.WindowsRegistry;
+import org.apache.commons.lang3.SystemUtils;
import picocli.CommandLine;
-import java.net.URI;
-import java.net.URISyntaxException;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
+import java.util.function.Supplier;
/** Installs the agent system-globally. */
public class Installer {
- private static final Path DEFAULT_INSTALL_DIRECTORY = Paths.get("/opt/teamscale-profiler/java");
+ private static final Path DEFAULT_INSTALL_DIRECTORY = windowsOrLinux(
+ () -> Paths.get(System.getenv("ProgramFiles")).resolve("teamscale-profiler/java"),
+ () -> Paths.get("/opt/teamscale-profiler/java")
+ );
+
private static final Path DEFAULT_ETC_DIRECTORY = Paths.get("/etc");
- private static final String RERUN_ADVICE = getRerunAdvice();
+ private static final String RERUN_ADVICE = windowsOrLinux(
+ () -> "Try running this installer as Administrator.",
+ () -> "Try running this installer as root, e.g. with sudo."
+ );
+
+ private static final String RESTART_ADVICE = windowsOrLinux(
+ () -> "Please restart Windows to apply all changes.",
+ () -> "In an interactive session, you have to log out and log back in for the changes to take effect."
+ );
- private static String getRerunAdvice() {
- if (SystemUtils.isWindows()) {
- return "Try running this installer as Administrator.";
+ private static T windowsOrLinux(Supplier windowsSupplier, Supplier linuxSupplier) {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ return windowsSupplier.get();
} else {
- return "Try running this installer as root, e.g. with sudo.";
+ return linuxSupplier.get();
}
}
-
/** Returns the directory that contains the agent to install or null if it can't be resolved. */
- private static Path getDefaultSourceDirectory() {
- try {
- URI jarFileUri = Installer.class.getProtectionDomain().getCodeSource().getLocation().toURI();
- // we assume that the dist zip is extracted and the installer jar not moved
- return Paths.get(jarFileUri).getParent();
- } catch (URISyntaxException e) {
- throw new IllegalStateException("Failed to obtain agent directory. This is a bug, please report it.", e);
+ private static Path getDefaultSourceDirectory() throws FatalInstallerError {
+ // since we package with jlink, java.home is guaranteed to point to SOURCEDIR/installer/installer-PLATFORM
+ Path jlinkJvmPath = Paths.get(System.getProperty("java.home"));
+ if (!Files.exists(jlinkJvmPath)) {
+ throw new FatalInstallerError(
+ "The JLink JVM path " + jlinkJvmPath + " does not exist."
+ + " It looks like you moved the installation files after extracting the zip."
+ + "\nPlease start over by extracting the profiler files from the zip file you downloaded."
+ + " Do not make any changes to the extracted files and directories or installation will fail.");
+ }
+
+ Path sourceDirectory = jlinkJvmPath.getParent().getParent();
+ if (!Files.exists(sourceDirectory)) {
+ throw new FatalInstallerError(
+ "The source directory " + sourceDirectory + " does not exist."
+ + " It looks like you moved the installation files after extracting the zip."
+ + "\nPlease start over by extracting the profiler files from the zip file you downloaded."
+ + " Do not make any changes to the extracted files and directories or installation will fail.");
}
+
+ return sourceDirectory;
}
/**
@@ -57,10 +84,13 @@ private static Path getDefaultSourceDirectory() {
* @param sourceDirectory directory that contains the profiler binaries and support files to install.
* @param installDirectory directory to which to install the profiler.
* @param etcDirectory on Linux: the /etc directory
+ * @param registry the Windows registry (not used on Linux)
*/
- public Installer(Path sourceDirectory, Path installDirectory, Path etcDirectory, boolean reloadSystemdDaemon) {
- EnvironmentMap environmentVariables = getEnvironmentVariables(installDirectory);
+ public Installer(Path sourceDirectory, Path installDirectory, Path etcDirectory, boolean reloadSystemdDaemon,
+ IRegistry registry) {
+ JvmEnvironmentMap environmentVariables = getEnvironmentVariables(installDirectory);
this.steps = Arrays.asList(new InstallAgentFilesStep(sourceDirectory, installDirectory),
+ new InstallWindowsSystemEnvironmentStep(environmentVariables, registry),
new InstallEtcEnvironmentStep(etcDirectory, environmentVariables),
new InstallSystemdStep(etcDirectory, environmentVariables, reloadSystemdDaemon));
}
@@ -73,22 +103,19 @@ public Installer(Path sourceDirectory, Path installDirectory, Path etcDirectory,
public static int install(TeamscaleCredentials credentials) {
try {
getDefaultInstaller().runInstall(credentials);
- System.out.println("Installation successful. Profiler installed to " + DEFAULT_INSTALL_DIRECTORY);
- if (SystemUtils.isLinux()) {
- System.out.println("To use the profiler in your current session, please log out and back in.");
- }
- System.out.println("To activate the profiler for an application, set the environment variable:"
- + "\nTEAMSCALE_JAVA_PROFILER_CONFIG_ID"
- + "\nIts value must be a valid profiler configuration ID defined in the Teamscale instance."
- + "\nThen, restart your application (for web applications: restart the app server)."
- + "\nIn an interactive session, you have to log out and log back in for the changes to take effect.");
+ System.out.println("Installation successful. Profiler installed to " + DEFAULT_INSTALL_DIRECTORY
+ + "\n\nTo activate the profiler for an application, set the environment variable:"
+ + "\nTEAMSCALE_JAVA_PROFILER_CONFIG_ID"
+ + "\nIts value must be a valid profiler configuration ID defined in the Teamscale instance."
+ + "\nThen, restart your application (for web applications: restart the app server)."
+ + "\n\n" + RESTART_ADVICE);
return CommandLine.ExitCode.OK;
} catch (PermissionError e) {
e.printToStderr();
System.err.println(
"\n\nInstallation failed because the installer had insufficient permissions to make the necessary"
- + " changes on your system.\nSee above for error messages.\n" + RERUN_ADVICE);
+ + " changes on your system.\nSee above for error messages.\n\n" + RERUN_ADVICE);
return RootCommand.EXIT_CODE_PERMISSION_ERROR;
} catch (FatalInstallerError e) {
e.printToStderr();
@@ -97,8 +124,9 @@ public static int install(TeamscaleCredentials credentials) {
}
}
- private static Installer getDefaultInstaller() {
- return new Installer(getDefaultSourceDirectory(), DEFAULT_INSTALL_DIRECTORY, DEFAULT_ETC_DIRECTORY, true);
+ private static Installer getDefaultInstaller() throws FatalInstallerError {
+ return new Installer(getDefaultSourceDirectory(), DEFAULT_INSTALL_DIRECTORY, DEFAULT_ETC_DIRECTORY, true,
+ WindowsRegistry.INSTANCE);
}
@@ -107,7 +135,7 @@ private static Installer getDefaultInstaller() {
*
* @return the exit code for the CLI.
*/
- public static int uninstall() {
+ public static int uninstall() throws FatalInstallerError {
UninstallerErrorReporter errorReporter = getDefaultInstaller().runUninstall();
if (errorReporter.errorsReported) {
String message = "Uninstallation failed. See above for error messages.";
@@ -117,8 +145,8 @@ public static int uninstall() {
System.err.println("\n\n" + message);
return RootCommand.EXIT_CODE_OTHER_ERROR;
}
- System.out.println("Profiler successfully uninstalled.\n" +
- "You need to restart all previously profiled applications to stop profiling them.");
+ System.out.println("Profiler successfully uninstalled. Please restart your computer.\n" +
+ "You need to restart all previously profiled applications to stop profiling them.");
return CommandLine.ExitCode.OK;
}
@@ -130,6 +158,9 @@ public static int uninstall() {
public void runInstall(TeamscaleCredentials credentials) throws FatalInstallerError {
TeamscaleUtils.checkTeamscaleConnection(credentials);
for (IStep step : steps) {
+ if (!step.shouldRun()) {
+ continue;
+ }
step.install(credentials);
}
}
@@ -140,7 +171,12 @@ public void runInstall(TeamscaleCredentials credentials) throws FatalInstallerEr
*/
public UninstallerErrorReporter runUninstall() {
UninstallerErrorReporter errorReporter = new UninstallerErrorReporter();
- for (IStep step : CollectionUtils.reverse(steps)) {
+ for (int i = steps.size() - 1; i >= 0; i--) {
+ IStep step = steps.get(i);
+ if (!step.shouldRun()) {
+ continue;
+ }
+
step.uninstall(errorReporter);
if (errorReporter.errorsReported) {
break;
@@ -180,9 +216,9 @@ public void report(FatalInstallerError e) {
* by application start scripts
*
*/
- private EnvironmentMap getEnvironmentVariables(Path installDirectory) {
+ private JvmEnvironmentMap getEnvironmentVariables(Path installDirectory) {
String javaAgentArgument = "-javaagent:" + getAgentJarPath(installDirectory);
- return new EnvironmentMap("JAVA_TOOL_OPTIONS", javaAgentArgument,
+ return new JvmEnvironmentMap("JAVA_TOOL_OPTIONS", javaAgentArgument,
"_JAVA_OPTIONS", javaAgentArgument);
}
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/JvmEnvironmentMap.java b/installer/src/main/java/com/teamscale/profiler/installer/JvmEnvironmentMap.java
new file mode 100644
index 000000000..14a8e75c6
--- /dev/null
+++ b/installer/src/main/java/com/teamscale/profiler/installer/JvmEnvironmentMap.java
@@ -0,0 +1,70 @@
+package com.teamscale.profiler.installer;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Map of JVM-specific environment variables and their values. Also handles quoting to ensure the environment variables
+ * are parsed correctly by the JVM.
+ */
+public class JvmEnvironmentMap {
+
+ private final Map environment = new HashMap<>();
+
+ /**
+ * Creates a map of keys and values from the supplied ones. Supplying e.g. A, B, C, D will store environment
+ * variables A=B and C=D.
+ */
+ public JvmEnvironmentMap(String... keysAndValues) {
+ for (int i = 0; i < keysAndValues.length; i += 2) {
+ environment.put(keysAndValues[i], keysAndValues[i + 1]);
+ }
+ }
+
+ /**
+ * Returns a map of environment variable names to their values. Values are quoted if necessary.
+ */
+ public Map getEnvironmentVariableMap() {
+ Map result = new HashMap<>();
+ for (String key : environment.keySet()) {
+ result.put(key, quoteIfNecessary(environment.get(key)));
+ }
+ return result;
+ }
+
+ /**
+ * The java command-line will treat single and double quotes as quoting and spaces as argument separators. All other
+ * characters are treated verbatim, including backslash. We simply assume that no quotes are used in the environment
+ * variable values. Therefore, we only need to quote arguments that contain spaces.
+ */
+ private String quoteIfNecessary(String value) {
+ if (!value.contains(" ")) {
+ return value;
+ }
+ return "\"" + value + "\"";
+ }
+
+ private Stream> sortedEntryStream() {
+ return environment.entrySet().stream().sorted(Map.Entry.comparingByKey());
+ }
+
+ /**
+ * Returns a list of lines that can be appended to /etc/environment. Lines are quoted if necessary.
+ */
+ public List getEtcEnvironmentLinesList() {
+ return sortedEntryStream().map(entry -> entry.getKey() + "=" + quoteIfNecessary(entry.getValue())).toList();
+ }
+
+ /**
+ * Returns a string that can be used in the DefaultEnvironment setting of a global systemd config file. Entries are
+ * quoted if necessary.
+ */
+ public String getSystemdString() {
+ return sortedEntryStream().map(entry -> quoteIfNecessary(entry.getKey() + "=" + entry.getValue()))
+ .collect(Collectors.joining(" "));
+ }
+
+}
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/RootCommand.java b/installer/src/main/java/com/teamscale/profiler/installer/RootCommand.java
index c428edd2d..96626e37d 100644
--- a/installer/src/main/java/com/teamscale/profiler/installer/RootCommand.java
+++ b/installer/src/main/java/com/teamscale/profiler/installer/RootCommand.java
@@ -1,6 +1,7 @@
package com.teamscale.profiler.installer;
-import org.conqat.lib.commons.system.SystemUtils;
+import com.teamscale.profiler.installer.utils.TeamscaleUtils;
+import org.apache.commons.lang3.SystemUtils;
import picocli.CommandLine;
/**
@@ -44,7 +45,7 @@ public static void main(String[] args) {
private static String getRootCommandName() {
String rootCommandName = "installer";
- if (SystemUtils.isWindows()) {
+ if (SystemUtils.IS_OS_WINDOWS) {
return rootCommandName.concat(".exe");
}
return rootCommandName;
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/steps/IStep.java b/installer/src/main/java/com/teamscale/profiler/installer/steps/IStep.java
index 1cb2fa4b1..004dbcf05 100644
--- a/installer/src/main/java/com/teamscale/profiler/installer/steps/IStep.java
+++ b/installer/src/main/java/com/teamscale/profiler/installer/steps/IStep.java
@@ -20,6 +20,11 @@ public interface IStep {
*/
void uninstall(IUninstallErrorReporter errorReporter);
+ /** Determines whether this step should not be run, e.g. because this step is not applicable to the current OS. */
+ default boolean shouldRun() {
+ return true;
+ }
+
/**
* Used to report errors that happen during uninstallation. During uninstalling, we want to remove everything we can
* remove, even if some parts of the process fail. Thus we don't just throw exceptions and abort.
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallAgentFilesStep.java b/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallAgentFilesStep.java
index ce33119b9..e87bc4ec5 100644
--- a/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallAgentFilesStep.java
+++ b/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallAgentFilesStep.java
@@ -1,10 +1,11 @@
package com.teamscale.profiler.installer.steps;
import com.teamscale.profiler.installer.FatalInstallerError;
-import com.teamscale.profiler.installer.InstallFileUtils;
+import com.teamscale.profiler.installer.utils.InstallFileUtils;
import com.teamscale.profiler.installer.PermissionError;
import com.teamscale.profiler.installer.TeamscaleCredentials;
-import org.conqat.lib.commons.filesystem.FileSystemUtils;
+import org.apache.commons.io.file.PathUtils;
+import org.apache.commons.lang3.SystemUtils;
import java.io.IOException;
import java.io.OutputStream;
@@ -26,14 +27,6 @@ public InstallAgentFilesStep(Path sourceDirectory, Path installDirectory) {
this.installDirectory = installDirectory;
}
- private Path getCoverageDirectory() {
- return installDirectory.resolve("coverage");
- }
-
- private Path getLogDirectory() {
- return installDirectory.resolve("logs");
- }
-
private Path getTeamscalePropertiesPath() {
return installDirectory.resolve("teamscale.properties");
}
@@ -44,7 +37,6 @@ public void install(TeamscaleCredentials credentials) throws FatalInstallerError
createAgentDirectory();
copyAgentFiles();
writeTeamscaleProperties(credentials);
- makeCoverageAndLogDirectoriesWorldWritable();
makeAllProfilerFilesWorldReadable();
}
@@ -53,21 +45,26 @@ private void ensureAgentIsPresentInSourceDirectory() throws FatalInstallerError
if (!Files.exists(agentPath)) {
throw new FatalInstallerError(
"It looks like you moved the installer. Could not locate the profiler files at " + sourceDirectory + "."
- + "\nPlease start over by extracting the profiler files from the zip file you downloaded."
- + " Do not make any changes to the extracted files and directories or installation will fail.");
+ + "\nPlease start over by extracting the profiler files from the zip file you downloaded."
+ + " Do not make any changes to the extracted files and directories or installation will fail.");
}
}
@Override
public void uninstall(IUninstallErrorReporter errorReporter) {
- if (!Files.exists(installDirectory)) {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ System.out.println("Please manually delete " + installDirectory);
return;
}
- FileSystemUtils.deleteRecursively(installDirectory.toFile());
+ if (!Files.exists(installDirectory)) {
+ return;
+ }
- if (Files.exists(installDirectory)) {
- errorReporter.report(new PermissionError("Failed to fully remove " + installDirectory));
+ try {
+ PathUtils.deleteDirectory(installDirectory);
+ } catch (IOException e) {
+ errorReporter.report(new FatalInstallerError("Failed to fully delete directory " + installDirectory, e));
}
}
@@ -82,15 +79,6 @@ private void makeAllProfilerFilesWorldReadable() throws FatalInstallerError {
}
}
- private void makeCoverageAndLogDirectoriesWorldWritable() throws FatalInstallerError {
- InstallFileUtils.createDirectory(getCoverageDirectory());
- InstallFileUtils.makeWritable(getCoverageDirectory());
-
- InstallFileUtils.createDirectory(getLogDirectory());
- InstallFileUtils.makeWritable(getLogDirectory());
-
- }
-
private void writeTeamscaleProperties(TeamscaleCredentials credentials) throws FatalInstallerError {
Properties properties = new Properties();
properties.setProperty("url", credentials.url.toString());
@@ -109,10 +97,10 @@ private void writeTeamscaleProperties(TeamscaleCredentials credentials) throws F
private void copyAgentFiles() throws FatalInstallerError {
try {
- FileSystemUtils.copyFiles(sourceDirectory.toFile(), installDirectory.toFile(), null);
+ PathUtils.copyDirectory(sourceDirectory, installDirectory);
} catch (IOException e) {
throw new PermissionError("Failed to copy some files to " + installDirectory + "."
- + " Please manually clean up " + installDirectory, e);
+ + " Please manually clean up " + installDirectory, e);
}
}
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallEtcEnvironmentStep.java b/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallEtcEnvironmentStep.java
index 7c4e3ad0c..41f412f2f 100644
--- a/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallEtcEnvironmentStep.java
+++ b/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallEtcEnvironmentStep.java
@@ -1,11 +1,10 @@
package com.teamscale.profiler.installer.steps;
-import com.teamscale.profiler.installer.EnvironmentMap;
+import com.teamscale.profiler.installer.JvmEnvironmentMap;
import com.teamscale.profiler.installer.FatalInstallerError;
import com.teamscale.profiler.installer.PermissionError;
import com.teamscale.profiler.installer.TeamscaleCredentials;
-import org.conqat.lib.commons.string.StringUtils;
-import org.conqat.lib.commons.system.SystemUtils;
+import org.apache.commons.lang3.SystemUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -21,33 +20,35 @@
public class InstallEtcEnvironmentStep implements IStep {
private final Path etcDirectory;
- private final EnvironmentMap environmentVariables;
+ private final JvmEnvironmentMap environmentVariables;
- public InstallEtcEnvironmentStep(Path etcDirectory, EnvironmentMap environmentMap) {
+ public InstallEtcEnvironmentStep(Path etcDirectory, JvmEnvironmentMap environmentMap) {
this.etcDirectory = etcDirectory;
this.environmentVariables = environmentMap;
}
@Override
- public void install(TeamscaleCredentials credentials) throws FatalInstallerError {
- if (!SystemUtils.isLinux()) {
- return;
- }
+ public boolean shouldRun() {
+ return SystemUtils.IS_OS_LINUX;
+ }
+ @Override
+ public void install(TeamscaleCredentials credentials) throws FatalInstallerError {
Path environmentFile = getEnvironmentFile();
+ String etcEnvironmentAddition = String.join("\n", environmentVariables.getEtcEnvironmentLinesList());
+
if (!Files.exists(environmentFile)) {
System.err.println(
environmentFile + " does not exist. Skipping system-wide registration of the profiler."
- + "\nYou need to manually register the profiler for process that should be profiled by"
- + " setting the following environment variables:"
- + "\n\n" + environmentVariables.getEtcEnvironmentString() + "\n");
+ + "\nYou need to manually register the profiler for process that should be profiled by"
+ + " setting the following environment variables:"
+ + "\n\n" + etcEnvironmentAddition + "\n");
return;
}
- String content = "\n" + environmentVariables.getEtcEnvironmentString() + "\n";
-
+ String content = "\n" + etcEnvironmentAddition + "\n";
try {
- Files.write(environmentFile, content.getBytes(StandardCharsets.US_ASCII),
+ Files.writeString(environmentFile, content, StandardCharsets.US_ASCII,
StandardOpenOption.APPEND);
} catch (IOException e) {
throw new PermissionError("Could not change contents of " + environmentFile, e);
@@ -60,10 +61,6 @@ private Path getEnvironmentFile() {
@Override
public void uninstall(IUninstallErrorReporter errorReporter) {
- if (!SystemUtils.isLinux()) {
- return;
- }
-
Path environmentFile = getEnvironmentFile();
if (!Files.exists(environmentFile)) {
return;
@@ -72,17 +69,16 @@ public void uninstall(IUninstallErrorReporter errorReporter) {
try {
List lines = Files.readAllLines(environmentFile, StandardCharsets.US_ASCII);
String newContent = removeProfilerVariables(lines);
- Files.write(environmentFile, newContent.getBytes(StandardCharsets.US_ASCII));
+ Files.writeString(environmentFile, newContent, StandardCharsets.US_ASCII);
} catch (IOException e) {
errorReporter.report(new PermissionError("Failed to remove profiler from " + environmentFile + "." +
- " Please remove the relevant environment variables yourself." +
- " Otherwise, Java applications may crash.", e));
+ " Please remove the relevant environment variables yourself." +
+ " Otherwise, Java applications may crash.", e));
}
}
private String removeProfilerVariables(List linesWithoutNewline) {
- Set linesToRemove = new HashSet<>(StringUtils.splitLinesAsList(
- environmentVariables.getEtcEnvironmentString(), false));
+ Set linesToRemove = new HashSet<>(environmentVariables.getEtcEnvironmentLinesList());
return linesWithoutNewline.stream().filter(line -> !linesToRemove.contains(line))
.collect(Collectors.joining("\n"));
}
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallSystemdStep.java b/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallSystemdStep.java
index 07c758bf4..ca4cc661c 100644
--- a/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallSystemdStep.java
+++ b/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallSystemdStep.java
@@ -1,14 +1,13 @@
package com.teamscale.profiler.installer.steps;
-import com.teamscale.profiler.installer.EnvironmentMap;
+import com.teamscale.profiler.installer.JvmEnvironmentMap;
import com.teamscale.profiler.installer.FatalInstallerError;
import com.teamscale.profiler.installer.PermissionError;
import com.teamscale.profiler.installer.TeamscaleCredentials;
-import org.conqat.lib.commons.system.SystemUtils;
+import org.apache.commons.lang3.SystemUtils;
import java.io.File;
import java.io.IOException;
-import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
@@ -20,21 +19,22 @@
public class InstallSystemdStep implements IStep {
private final Path etcDirectory;
- private final EnvironmentMap environmentVariables;
+ private final JvmEnvironmentMap environmentVariables;
private final boolean reloadSystemdDaemon;
- public InstallSystemdStep(Path etcDirectory, EnvironmentMap environmentMap, boolean reloadSystemdDaemon) {
+ public InstallSystemdStep(Path etcDirectory, JvmEnvironmentMap environmentMap, boolean reloadSystemdDaemon) {
this.etcDirectory = etcDirectory;
this.environmentVariables = environmentMap;
this.reloadSystemdDaemon = reloadSystemdDaemon;
}
@Override
- public void install(TeamscaleCredentials credentials) throws FatalInstallerError {
- if (!SystemUtils.isLinux()) {
- return;
- }
+ public boolean shouldRun() {
+ return SystemUtils.IS_OS_LINUX;
+ }
+ @Override
+ public void install(TeamscaleCredentials credentials) throws FatalInstallerError {
if (!Files.exists(getSystemdEtcDirectory())) {
System.out.println("systemd could not be detected. Not installing profiler for systemd services.");
// system has no systemd installed
@@ -54,12 +54,12 @@ public void install(TeamscaleCredentials credentials) throws FatalInstallerError
if (Files.exists(systemdConfigFile)) {
throw new PermissionError(
"Cannot create systemd configuration file " + systemdConfigFile + " because it already exists." +
- "\nPlease uninstall any old profiler versions first");
+ "\nPlease uninstall any old profiler versions first");
}
String content = "[Manager]\nDefaultEnvironment=" + environmentVariables.getSystemdString() + "\n";
try {
- Files.write(systemdConfigFile, content.getBytes(StandardCharsets.UTF_8));
+ Files.writeString(systemdConfigFile, content);
} catch (IOException e) {
throw new PermissionError("Could not create " + systemdConfigFile, e);
}
@@ -91,10 +91,10 @@ private void daemonReload() {
}
private void askUserToManuallyReloadDaemon() {
- System.err.println(
- "Failed to reload the systemd daemon. Systemd services can only be profiled after reloading the daemon." +
- "\nPlease manually reload the daemon with:" +
- "\nsystemctl daemon-reload");
+ System.err.println("""
+ Failed to reload the systemd daemon. Systemd services can only be profiled after reloading the daemon.
+ Please manually reload the daemon with:
+ systemctl daemon-reload""");
}
private Path getSystemdEtcDirectory() {
@@ -111,10 +111,6 @@ private Path getSystemdConfigFile() {
@Override
public void uninstall(IUninstallErrorReporter errorReporter) {
- if (!SystemUtils.isLinux()) {
- return;
- }
-
Path systemdConfigFile = getSystemdConfigFile();
if (!Files.exists(systemdConfigFile)) {
return;
@@ -125,7 +121,7 @@ public void uninstall(IUninstallErrorReporter errorReporter) {
} catch (IOException e) {
errorReporter.report(
new PermissionError("Failed to remove systemd config file " + systemdConfigFile + "." +
- " Manually remove this file or systemd Java services may fail to start.", e));
+ " Manually remove this file or systemd Java services may fail to start.", e));
}
daemonReload();
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallWindowsSystemEnvironmentStep.java b/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallWindowsSystemEnvironmentStep.java
new file mode 100644
index 000000000..2aff825f9
--- /dev/null
+++ b/installer/src/main/java/com/teamscale/profiler/installer/steps/InstallWindowsSystemEnvironmentStep.java
@@ -0,0 +1,92 @@
+package com.teamscale.profiler.installer.steps;
+
+import com.teamscale.profiler.installer.JvmEnvironmentMap;
+import com.teamscale.profiler.installer.FatalInstallerError;
+import com.teamscale.profiler.installer.TeamscaleCredentials;
+import com.teamscale.profiler.installer.windows.IRegistry;
+import com.teamscale.profiler.installer.windows.WindowsRegistry;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.SystemUtils;
+
+import java.util.Map;
+
+/** On Windows, registers the agent globally via environment variables set for the entire machine. */
+public class InstallWindowsSystemEnvironmentStep implements IStep {
+
+ private final JvmEnvironmentMap environmentVariables;
+ private final IRegistry registry;
+
+ public InstallWindowsSystemEnvironmentStep(JvmEnvironmentMap environmentMap, IRegistry registry) {
+ this.environmentVariables = environmentMap;
+ this.registry = registry;
+ }
+
+ @Override
+ public boolean shouldRun() {
+ return SystemUtils.IS_OS_WINDOWS;
+ }
+
+ @Override
+ public void install(TeamscaleCredentials credentials) throws FatalInstallerError {
+ Map map = environmentVariables.getEnvironmentVariableMap();
+ for (String variable : map.keySet()) {
+ addProfiler(variable, map.get(variable), registry);
+ }
+ }
+
+ @Override
+ public void uninstall(IUninstallErrorReporter errorReporter) {
+ Map map = environmentVariables.getEnvironmentVariableMap();
+ for (String variable : map.keySet()) {
+ try {
+ String valueToRemove = map.get(variable);
+ removeProfiler(variable, valueToRemove, registry);
+ } catch (FatalInstallerError e) {
+ errorReporter.report(e);
+ }
+ }
+ }
+
+ /**
+ * Adds the profiler to the given registry under the given variable, appending it in case the variable already has a
+ * value set.
+ */
+ /*package*/
+ static void addProfiler(String variable, String valueToAdd, IRegistry registry) throws FatalInstallerError {
+ String currentValue = registry.getHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, variable);
+
+ String newValue = valueToAdd;
+ if (!StringUtils.isEmpty(currentValue)) {
+ newValue = valueToAdd + " " + currentValue;
+ }
+ registry.setHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, variable, newValue);
+ }
+
+ /**
+ * Removes the profiler from the given registry under the given variable, leaving any other parts of the variable in
+ * place.
+ */
+ /*package*/
+ static void removeProfiler(String variable, String valueToRemove,
+ IRegistry registry) throws FatalInstallerError {
+ String currentValue = registry.getHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, variable);
+ if (StringUtils.isEmpty(currentValue)) {
+ return;
+ }
+
+ if (currentValue.equals(valueToRemove)) {
+ registry.deleteHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, variable);
+ return;
+ }
+
+ if (!currentValue.contains(valueToRemove)) {
+ return;
+ }
+
+ if (currentValue.contains(valueToRemove + " ")) {
+ valueToRemove += " ";
+ }
+ String newValue = currentValue.replace(valueToRemove, "");
+ registry.setHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, variable, newValue);
+ }
+}
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/InstallFileUtils.java b/installer/src/main/java/com/teamscale/profiler/installer/utils/InstallFileUtils.java
similarity index 70%
rename from installer/src/main/java/com/teamscale/profiler/installer/InstallFileUtils.java
rename to installer/src/main/java/com/teamscale/profiler/installer/utils/InstallFileUtils.java
index ffeea0ca6..3bb4cd9f7 100644
--- a/installer/src/main/java/com/teamscale/profiler/installer/InstallFileUtils.java
+++ b/installer/src/main/java/com/teamscale/profiler/installer/utils/InstallFileUtils.java
@@ -1,4 +1,7 @@
-package com.teamscale.profiler.installer;
+package com.teamscale.profiler.installer.utils;
+
+import com.teamscale.profiler.installer.FatalInstallerError;
+import com.teamscale.profiler.installer.PermissionError;
import java.io.IOException;
import java.nio.file.Files;
@@ -15,14 +18,6 @@ public static void makeReadable(Path path) throws FatalInstallerError {
}
}
- /** Makes the given path world-writable. */
- public static void makeWritable(Path path) throws FatalInstallerError {
- if (!path.toFile().setWritable(true, false)) {
- throw new PermissionError(
- "Failed to make " + path + " writable. Please check file permissions.");
- }
- }
-
/** Creates the given directory, handling errors. */
public static void createDirectory(Path directory) throws FatalInstallerError {
try {
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/OkHttpUtils.java b/installer/src/main/java/com/teamscale/profiler/installer/utils/OkHttpUtils.java
similarity index 98%
rename from installer/src/main/java/com/teamscale/profiler/installer/OkHttpUtils.java
rename to installer/src/main/java/com/teamscale/profiler/installer/utils/OkHttpUtils.java
index 3829a4318..91146d91d 100644
--- a/installer/src/main/java/com/teamscale/profiler/installer/OkHttpUtils.java
+++ b/installer/src/main/java/com/teamscale/profiler/installer/utils/OkHttpUtils.java
@@ -1,4 +1,4 @@
-package com.teamscale.profiler.installer;
+package com.teamscale.profiler.installer.utils;
import okhttp3.OkHttpClient;
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/TeamscaleUtils.java b/installer/src/main/java/com/teamscale/profiler/installer/utils/TeamscaleUtils.java
similarity index 66%
rename from installer/src/main/java/com/teamscale/profiler/installer/TeamscaleUtils.java
rename to installer/src/main/java/com/teamscale/profiler/installer/utils/TeamscaleUtils.java
index edcf9b1b8..72b7594e8 100644
--- a/installer/src/main/java/com/teamscale/profiler/installer/TeamscaleUtils.java
+++ b/installer/src/main/java/com/teamscale/profiler/installer/utils/TeamscaleUtils.java
@@ -1,5 +1,7 @@
-package com.teamscale.profiler.installer;
+package com.teamscale.profiler.installer.utils;
+import com.teamscale.profiler.installer.FatalInstallerError;
+import com.teamscale.profiler.installer.TeamscaleCredentials;
import okhttp3.Credentials;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
@@ -23,6 +25,8 @@ public static void disableSslValidation() {
}
/** Ensures that we can connect to Teamscale with the given credentials. */
+ // this IntelliJ warning is a false positive due to the okhttp property and method overloads
+ @SuppressWarnings("KotlinInternalInJava")
public static void checkTeamscaleConnection(TeamscaleCredentials credentials) throws FatalInstallerError {
OkHttpClient client = OkHttpUtils.createClient(validateSsl, 30);
@@ -43,24 +47,24 @@ public static void checkTeamscaleConnection(TeamscaleCredentials credentials) th
handleErrors(response, credentials);
} catch (SSLException e) {
throw new FatalInstallerError("Failed to connect via HTTPS to " + credentials.url
- + "\nPlease ensure that your Teamscale instance is reachable under " + credentials.url
- + " and that it is configured for HTTPS, not HTTP. E.g. open that URL in your"
- + " browser and verify that you can connect successfully."
- + "\n\nIf you want to accept self-signed or broken certificates without an error"
- + " you can use --insecure.",
+ + "\nPlease ensure that your Teamscale instance is reachable under " + credentials.url
+ + " and that it is configured for HTTPS, not HTTP. E.g. open that URL in your"
+ + " browser and verify that you can connect successfully."
+ + "\n\nIf you want to accept self-signed or broken certificates without an error"
+ + " you can use --insecure.",
e);
} catch (UnknownHostException e) {
- throw new FatalInstallerError("The host " + url + " could not be resolved."
- + " Please ensure you have no typo and that this host is reachable from this server.",
+ throw new FatalInstallerError("The host " + credentials.url + " could not be resolved."
+ + " Please ensure you have no typo and that this host is reachable from this server.",
e);
} catch (ConnectException e) {
- throw new FatalInstallerError("The URL " + url + " refused a connection."
- + " Please ensure that you have no typo and that this endpoint is reachable and not blocked by firewalls.",
+ throw new FatalInstallerError("The host " + credentials.url + " refused a connection."
+ + " Please ensure that you have no typo and that this endpoint is reachable and not blocked by firewalls.",
e);
} catch (
SocketTimeoutException e) {
throw new FatalInstallerError("Request timeout reached."
- + " Please ensure that you have no typo and that this endpoint is reachable and not blocked by firewalls.",
+ + " Please ensure that you have no typo and that this endpoint is reachable and not blocked by firewalls.",
e);
} catch (IOException e) {
throw new FatalInstallerError(
@@ -75,18 +79,18 @@ private static void handleErrors(Response response, TeamscaleCredentials credent
location = "";
}
throw new FatalInstallerError("You provided an incorrect URL."
- + " The server responded with a redirect to " + "'" + location + "'."
- + " This may e.g. happen if you used HTTP instead of HTTPS."
- + " Please use the correct URL for Teamscale instead.");
+ + " The server responded with a redirect to " + "'" + location + "'."
+ + " This may e.g. happen if you used HTTP instead of HTTPS."
+ + " Please use the correct URL for Teamscale instead.");
}
if (response.code() == 401) {
String editUserUrl = TeamscaleUtils.getEditUserUrl(credentials.url, credentials.username);
throw new FatalInstallerError("You provided incorrect credentials."
- + " Either the user '" + credentials.username + "' does not exist in Teamscale"
- + " or the access key you provided is incorrect."
- + " Please check both the username and access key in Teamscale under Admin > Users: "
- + editUserUrl + "\nPlease use the user's access key, not their password.");
+ + " Either the user '" + credentials.username + "' does not exist in Teamscale"
+ + " or the access key you provided is incorrect."
+ + " Please check both the username and access key in Teamscale under Admin > Users: "
+ + editUserUrl + "\nPlease use the user's access key, not their password.");
}
if (!response.isSuccessful()) {
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/windows/IRegistry.java b/installer/src/main/java/com/teamscale/profiler/installer/windows/IRegistry.java
new file mode 100644
index 000000000..502870cfb
--- /dev/null
+++ b/installer/src/main/java/com/teamscale/profiler/installer/windows/IRegistry.java
@@ -0,0 +1,22 @@
+package com.teamscale.profiler.installer.windows;
+
+import com.teamscale.profiler.installer.FatalInstallerError;
+
+/**
+ * Abstraction of the Windows registry to make registry usage testable.
+ */
+public interface IRegistry {
+
+ /**
+ * Reads a registry value inside a registry key in HKLM.
+ * Returns null if the value does not exist.
+ */
+ String getHklmValue(String key, String name) throws FatalInstallerError;
+
+ /** Sets a registry value inside a registry key in HKLM. */
+ void setHklmValue(String key, String name, String value) throws FatalInstallerError;
+
+ /** Deletes a registry value inside a registry key in HKLM. */
+ void deleteHklmValue(String key, String name) throws FatalInstallerError;
+
+}
diff --git a/installer/src/main/java/com/teamscale/profiler/installer/windows/WindowsRegistry.java b/installer/src/main/java/com/teamscale/profiler/installer/windows/WindowsRegistry.java
new file mode 100644
index 000000000..9be442fb0
--- /dev/null
+++ b/installer/src/main/java/com/teamscale/profiler/installer/windows/WindowsRegistry.java
@@ -0,0 +1,58 @@
+package com.teamscale.profiler.installer.windows;
+
+import com.sun.jna.platform.win32.Advapi32Util;
+import com.sun.jna.platform.win32.Win32Exception;
+import com.sun.jna.platform.win32.WinReg;
+import com.teamscale.profiler.installer.FatalInstallerError;
+
+/**
+ * Accesses the Windows registry.
+ */
+public class WindowsRegistry implements IRegistry {
+
+ /** The key under which machine-global environment variables are stored. */
+ public static final String ENVIRONMENT_REGISTRY_KEY = "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
+
+ /** The singleton instance. */
+ public static final WindowsRegistry INSTANCE = new WindowsRegistry();
+
+ private WindowsRegistry() {
+ // private constructor to force usage of singleton
+ }
+
+ @Override
+ public String getHklmValue(String key, String name) throws FatalInstallerError {
+ try {
+ if (!Advapi32Util.registryValueExists(WinReg.HKEY_LOCAL_MACHINE, key, name)) {
+ return null;
+ }
+ return Advapi32Util.registryGetStringValue(WinReg.HKEY_LOCAL_MACHINE, key, name);
+ } catch (Win32Exception e) {
+ throw new FatalInstallerError(
+ "Failed to read registry key HKLM\\" + key + ". Try running this installer as Administrator.", e);
+ }
+ }
+
+ @Override
+ public void setHklmValue(String key, String name, String value) throws FatalInstallerError {
+ try {
+ Advapi32Util.registrySetStringValue(WinReg.HKEY_LOCAL_MACHINE, key, name, value);
+ } catch (Win32Exception e) {
+ throw new FatalInstallerError(
+ "Failed to write registry key HKLM\\" + key + ". Try running this installer as Administrator.", e);
+ }
+ }
+
+ @Override
+ public void deleteHklmValue(String key, String name) throws FatalInstallerError {
+ try {
+ if (!Advapi32Util.registryValueExists(WinReg.HKEY_LOCAL_MACHINE, key, name)) {
+ return;
+ }
+ Advapi32Util.registryDeleteValue(WinReg.HKEY_LOCAL_MACHINE, key, name);
+ } catch (Win32Exception e) {
+ throw new FatalInstallerError(
+ "Failed to delete registry key HKLM\\" + key + ". Try running this installer as Administrator.", e);
+ }
+ }
+}
diff --git a/installer/src/main/java/module-info.java b/installer/src/main/java/module-info.java
new file mode 100644
index 000000000..5f4322bb4
--- /dev/null
+++ b/installer/src/main/java/module-info.java
@@ -0,0 +1,13 @@
+module com.teamscale.profiler.installer {
+ requires okhttp3;
+ requires info.picocli;
+ requires com.sun.jna.platform;
+ requires org.apache.commons.lang3;
+ requires org.apache.commons.io;
+ exports com.teamscale.profiler.installer;
+ opens com.teamscale.profiler.installer;
+ exports com.teamscale.profiler.installer.utils;
+ exports com.teamscale.profiler.installer.windows;
+ opens com.teamscale.profiler.installer.utils;
+}
+
diff --git a/installer/src/test/java/com/teamscale/profiler/installer/AllPlatformsInstallerTest.java b/installer/src/test/java/com/teamscale/profiler/installer/AllPlatformsInstallerTest.java
new file mode 100644
index 000000000..9156c8a3d
--- /dev/null
+++ b/installer/src/test/java/com/teamscale/profiler/installer/AllPlatformsInstallerTest.java
@@ -0,0 +1,145 @@
+package com.teamscale.profiler.installer;
+
+import com.teamscale.profiler.installer.utils.MockRegistry;
+import com.teamscale.profiler.installer.utils.MockTeamscale;
+import com.teamscale.profiler.installer.utils.TestUtils;
+import com.teamscale.test.commons.SystemTestUtils;
+import okhttp3.HttpUrl;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Properties;
+
+import static com.teamscale.profiler.installer.utils.UninstallErrorReporterAssert.assertThat;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class AllPlatformsInstallerTest {
+
+ private static final String FILE_TO_INSTALL_CONTENT = "install-me";
+ private static final String NESTED_FILE_CONTENT = "nested-file";
+ private static final String TEAMSCALE_URL = "http://localhost:" + SystemTestUtils.TEAMSCALE_PORT + "/";
+
+
+ private Path sourceDirectory;
+ private Path targetDirectory;
+ private Path etcDirectory;
+
+ private Path installedFile;
+ private Path installedNestedFile;
+ private Path installedTeamscaleProperties;
+
+ private static MockTeamscale mockTeamscale;
+
+ @BeforeEach
+ void setUpSourceDirectory() throws IOException {
+ sourceDirectory = Files.createTempDirectory("InstallerTest-source");
+ targetDirectory = Files.createTempDirectory("InstallerTest-target").resolve("profiler");
+ etcDirectory = Files.createTempDirectory("InstallerTest-etc");
+
+ Path fileToInstall = sourceDirectory.resolve("install-me.txt");
+ Files.writeString(fileToInstall, FILE_TO_INSTALL_CONTENT, StandardOpenOption.CREATE);
+
+ Path nestedFileToInstall = sourceDirectory.resolve("lib/teamscale-jacoco-agent.jar");
+ Files.createDirectories(nestedFileToInstall.getParent());
+ Files.writeString(nestedFileToInstall, NESTED_FILE_CONTENT,
+ StandardOpenOption.CREATE);
+
+ installedFile = targetDirectory.resolve(sourceDirectory.relativize(fileToInstall));
+ installedNestedFile = targetDirectory.resolve(sourceDirectory.relativize(nestedFileToInstall));
+ installedTeamscaleProperties = targetDirectory.resolve("teamscale.properties");
+ }
+
+ @BeforeAll
+ static void startFakeTeamscale() {
+ mockTeamscale = new MockTeamscale(SystemTestUtils.TEAMSCALE_PORT);
+ }
+
+ @AfterAll
+ static void stopFakeTeamscale() {
+ mockTeamscale.shutdown();
+ }
+
+ @Test
+ void successfulInstallation() throws FatalInstallerError, IOException {
+ install();
+
+ assertThat(installedFile).exists().content().isEqualTo(FILE_TO_INSTALL_CONTENT);
+ assertThat(installedNestedFile).exists().content().isEqualTo(NESTED_FILE_CONTENT);
+ assertThat(installedTeamscaleProperties).exists();
+
+ Properties properties = new Properties();
+ properties.load(Files.newInputStream(installedTeamscaleProperties));
+ assertThat(properties.keySet()).containsExactlyInAnyOrder("url", "username", "accesskey");
+ assertThat(properties.getProperty("url")).isEqualTo(TEAMSCALE_URL);
+ assertThat(properties.getProperty("username")).isEqualTo("user");
+ assertThat(properties.getProperty("accesskey")).isEqualTo("accesskey");
+ }
+
+ @Test
+ void distributionChangedByUser() throws IOException {
+ Files.delete(sourceDirectory.resolve("lib/teamscale-jacoco-agent.jar"));
+ assertThatThrownBy(this::install)
+ .hasMessageContaining("It looks like you moved the installer");
+ }
+
+ @Test
+ void successfulUninstallation() throws FatalInstallerError {
+ install();
+ Installer.UninstallerErrorReporter errorReporter = uninstall();
+ assertThat(errorReporter).hadNoErrors();
+ }
+
+ @Test
+ void nonexistantTeamscaleUrl() {
+ assertThatThrownBy(() -> install("http://does-not-exist:8080"))
+ .hasMessageContaining("could not be resolved");
+ assertThat(targetDirectory).doesNotExist();
+ }
+
+ @Test
+ void connectionRefused() {
+ assertThatThrownBy(() -> install("http://localhost:" + (SystemTestUtils.TEAMSCALE_PORT + 1)))
+ .hasMessageContaining("refused a connection");
+ assertThat(targetDirectory).doesNotExist();
+ }
+
+ @Test
+ void httpsInsteadOfHttp() {
+ assertThatThrownBy(() -> install("https://localhost:" + (int) SystemTestUtils.TEAMSCALE_PORT))
+ .hasMessageContaining("configured for HTTPS, not HTTP");
+ assertThat(targetDirectory).doesNotExist();
+ }
+
+ @Test
+ void profilerAlreadyInstalled() throws IOException {
+ Files.createDirectories(targetDirectory);
+ assertThatThrownBy(this::install).hasMessageContaining("Path already exists");
+ }
+
+ @Test
+ void installDirectoryNotWritable() throws Exception {
+ TestUtils.makePathReadOnly(targetDirectory.getParent());
+ assertThatThrownBy(() -> install(TEAMSCALE_URL)).hasMessageContaining("Cannot create directory");
+ }
+
+ private void install() throws FatalInstallerError {
+ install(TEAMSCALE_URL);
+ }
+
+ private void install(String teamscaleUrl) throws FatalInstallerError {
+ new Installer(sourceDirectory, targetDirectory, etcDirectory, false, new MockRegistry()).runInstall(
+ new TeamscaleCredentials(HttpUrl.get(teamscaleUrl), "user", "accesskey"));
+ }
+
+ private Installer.UninstallerErrorReporter uninstall() {
+ return new Installer(sourceDirectory, targetDirectory, etcDirectory, false, new MockRegistry()).runUninstall();
+ }
+
+}
diff --git a/installer/src/test/java/com/teamscale/profiler/installer/EnvironmentMapTest.java b/installer/src/test/java/com/teamscale/profiler/installer/EnvironmentMapTest.java
index 362f66f21..0f8ef3a52 100644
--- a/installer/src/test/java/com/teamscale/profiler/installer/EnvironmentMapTest.java
+++ b/installer/src/test/java/com/teamscale/profiler/installer/EnvironmentMapTest.java
@@ -7,19 +7,18 @@
class EnvironmentMapTest {
@Test
- void testEscaping() {
- assertThat(new EnvironmentMap("V", "a b").getSystemdString()).isEqualTo("\"V=a b\"");
- assertThat(new EnvironmentMap("V", "\\a").getSystemdString()).isEqualTo("\"V=\\\\a\"");
- assertThat(new EnvironmentMap("V", "\"").getSystemdString()).isEqualTo("\"V=\\\"\"");
- assertThat(new EnvironmentMap("V", "a b").getEtcEnvironmentString()).isEqualTo("V=\"a b\"");
- assertThat(new EnvironmentMap("V", "\\a").getEtcEnvironmentString()).isEqualTo("V=\"\\\\a\"");
- assertThat(new EnvironmentMap("V", "\"").getEtcEnvironmentString()).isEqualTo("V=\"\\\"\"");
+ void testQuoting() {
+ assertThat(new JvmEnvironmentMap("V", "a b").getSystemdString()).isEqualTo("\"V=a b\"");
+ assertThat(new JvmEnvironmentMap("V", "a b").getEtcEnvironmentLinesList()).containsExactlyInAnyOrder(
+ "V=\"a b\"");
+ assertThat(new JvmEnvironmentMap("V", "a b").getEnvironmentVariableMap()).containsEntry("V", "\"a b\"");
}
@Test
void testMultipleEntries() {
- assertThat(new EnvironmentMap("V", "a", "E", "b").getSystemdString()).isEqualTo("E=b V=a");
- assertThat(new EnvironmentMap("V", "a", "E", "b").getEtcEnvironmentString()).isEqualTo("E=b\nV=a");
+ assertThat(new JvmEnvironmentMap("V", "a", "E", "b").getSystemdString()).isEqualTo("E=b V=a");
+ assertThat(new JvmEnvironmentMap("V", "a", "E", "b").getEtcEnvironmentLinesList()).containsExactlyInAnyOrder(
+ "E=b", "V=a");
}
}
\ No newline at end of file
diff --git a/installer/src/test/java/com/teamscale/profiler/installer/InstallerTest.java b/installer/src/test/java/com/teamscale/profiler/installer/InstallerTest.java
deleted file mode 100644
index 56a672746..000000000
--- a/installer/src/test/java/com/teamscale/profiler/installer/InstallerTest.java
+++ /dev/null
@@ -1,263 +0,0 @@
-package com.teamscale.profiler.installer;
-
-import okhttp3.HttpUrl;
-import org.conqat.lib.commons.filesystem.FileSystemUtils;
-import org.conqat.lib.commons.system.SystemUtils;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.condition.EnabledOnOs;
-import org.junit.jupiter.api.condition.OS;
-
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.util.Properties;
-
-import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ;
-import static java.nio.file.attribute.PosixFilePermission.OTHERS_WRITE;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-@EnabledOnOs(OS.LINUX)
-class InstallerTest {
-
- private static final int TEAMSCALE_PORT = 8059;
- private static final String FILE_TO_INSTALL_CONTENT = "install-me";
- private static final String NESTED_FILE_CONTENT = "nested-file";
- private static final String ENVIRONMENT_CONTENT = "#this is /etc/environment\nPATH=/usr/bin";
- private static final String TEAMSCALE_URL = "http://localhost:" + TEAMSCALE_PORT + "/";
-
-
- private Path sourceDirectory;
- private Path targetDirectory;
- private Path etcDirectory;
-
- private Path installedFile;
- private Path installedNestedFile;
- private Path installedTeamscaleProperties;
- private Path installedCoverageDirectory;
- private Path installedLogsDirectory;
- private Path installedAgentLibrary;
-
- private Path environmentFile;
- private Path systemdDirectory;
- private Path systemdConfig;
-
- private static MockTeamscale mockTeamscale;
-
- @BeforeEach
- void setUpSourceDirectory() throws IOException {
- sourceDirectory = Files.createTempDirectory("InstallerTest-source");
- targetDirectory = Files.createTempDirectory("InstallerTest-target").resolve("profiler");
- etcDirectory = Files.createTempDirectory("InstallerTest-etc");
-
- environmentFile = etcDirectory.resolve("environment");
- Files.write(environmentFile, ENVIRONMENT_CONTENT.getBytes(StandardCharsets.UTF_8));
-
- systemdDirectory = etcDirectory.resolve("systemd");
- Files.createDirectory(systemdDirectory);
- systemdConfig = systemdDirectory.resolve("system.conf.d/teamscale-java-profiler.conf");
-
- Path fileToInstall = sourceDirectory.resolve("install-me.txt");
- Files.write(fileToInstall, FILE_TO_INSTALL_CONTENT.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE);
-
- Path nestedFileToInstall = sourceDirectory.resolve("lib/teamscale-jacoco-agent.jar");
- Files.createDirectories(nestedFileToInstall.getParent());
- Files.write(nestedFileToInstall, NESTED_FILE_CONTENT.getBytes(StandardCharsets.UTF_8),
- StandardOpenOption.CREATE);
-
- installedFile = targetDirectory.resolve(sourceDirectory.relativize(fileToInstall));
- installedNestedFile = targetDirectory.resolve(sourceDirectory.relativize(nestedFileToInstall));
- installedTeamscaleProperties = targetDirectory.resolve("teamscale.properties");
- installedCoverageDirectory = targetDirectory.resolve("coverage");
- installedLogsDirectory = targetDirectory.resolve("logs");
- installedAgentLibrary = targetDirectory.resolve("lib/teamscale-jacoco-agent.jar");
- }
-
- @BeforeAll
- static void startFakeTeamscale() {
- mockTeamscale = new MockTeamscale(TEAMSCALE_PORT);
- }
-
- @AfterAll
- static void stopFakeTeamscale() {
- mockTeamscale.shutdown();
- }
-
- @Test
- void successfulInstallation() throws FatalInstallerError, IOException {
- install();
-
- assertThat(installedFile).exists().content().isEqualTo(FILE_TO_INSTALL_CONTENT);
- assertThat(installedNestedFile).exists().content().isEqualTo(NESTED_FILE_CONTENT);
-
- assertThat(installedTeamscaleProperties).exists();
- Properties properties = FileSystemUtils.readProperties(installedTeamscaleProperties.toFile());
- assertThat(properties.keySet()).containsExactlyInAnyOrder("url", "username", "accesskey");
- assertThat(properties.getProperty("url")).isEqualTo(TEAMSCALE_URL);
- assertThat(properties.getProperty("username")).isEqualTo("user");
- assertThat(properties.getProperty("accesskey")).isEqualTo("accesskey");
-
- assertThat(installedCoverageDirectory).exists().isReadable().isWritable();
- assertThat(installedLogsDirectory).exists().isReadable().isWritable();
-
- if (SystemUtils.isLinux()) {
- assertThat(Files.getPosixFilePermissions(installedTeamscaleProperties)).contains(OTHERS_READ);
- assertThat(Files.getPosixFilePermissions(installedFile)).contains(OTHERS_READ);
- assertThat(Files.getPosixFilePermissions(installedLogsDirectory)).contains(OTHERS_READ, OTHERS_WRITE);
- assertThat(Files.getPosixFilePermissions(installedCoverageDirectory)).contains(OTHERS_READ, OTHERS_WRITE);
-
- assertThat(environmentFile).content().isEqualTo(ENVIRONMENT_CONTENT
- + "\nJAVA_TOOL_OPTIONS=-javaagent:" + installedAgentLibrary
- + "\n_JAVA_OPTIONS=-javaagent:" + installedAgentLibrary + "\n");
-
- assertThat(systemdConfig).content().isEqualTo("[Manager]"
- + "\nDefaultEnvironment=JAVA_TOOL_OPTIONS=-javaagent:" + installedAgentLibrary
- + " _JAVA_OPTIONS=-javaagent:" + installedAgentLibrary + "\n");
- }
- }
-
- @Test
- void distributionChangedByUser() throws IOException {
- Files.delete(sourceDirectory.resolve("lib/teamscale-jacoco-agent.jar"));
- assertThatThrownBy(this::install)
- .hasMessageContaining("It looks like you moved the installer");
- }
-
- @Test
- void successfulUninstallation() throws FatalInstallerError {
- install();
- Installer.UninstallerErrorReporter errorReporter = new Installer(sourceDirectory, targetDirectory,
- etcDirectory, false).runUninstall();
-
- assertThat(errorReporter.wereErrorsReported()).isFalse();
-
- assertThat(targetDirectory).doesNotExist();
- if (SystemUtils.isLinux()) {
- assertThat(environmentFile).exists().content().isEqualTo(ENVIRONMENT_CONTENT);
- assertThat(systemdConfig).doesNotExist();
- }
- }
-
- @Test
- void uninstallSuccessfullyEvenIfSystemDConfigWasManuallyRemoved() throws FatalInstallerError, IOException {
- install();
- Files.delete(systemdConfig);
- Installer.UninstallerErrorReporter errorReporter = new Installer(sourceDirectory, targetDirectory,
- etcDirectory, false).runUninstall();
-
- assertThat(errorReporter.wereErrorsReported()).isFalse();
- }
-
- @Test
- void uninstallSuccessfullyEvenIfEnvironmentFileDoesntExist() throws FatalInstallerError, IOException {
- install();
- Files.delete(environmentFile);
- Installer.UninstallerErrorReporter errorReporter = new Installer(sourceDirectory, targetDirectory,
- etcDirectory, false).runUninstall();
-
- assertThat(errorReporter.wereErrorsReported()).isFalse();
- }
-
- @Test
- void uninstallDeletingAgentDirectoryFails() throws FatalInstallerError {
- install();
- assertThat(targetDirectory.toFile().setWritable(false, false)).isTrue();
-
- Installer.UninstallerErrorReporter errorReporter = new Installer(sourceDirectory, targetDirectory,
- etcDirectory, false).runUninstall();
-
- assertThat(errorReporter.wereErrorsReported()).isTrue();
-
- assertThat(targetDirectory).exists();
- assertThat(installedTeamscaleProperties).exists();
- // nested files must be removed if possible
- assertThat(installedNestedFile).doesNotExist();
-
- if (SystemUtils.isLinux()) {
- assertThat(environmentFile).exists().content().isEqualTo(ENVIRONMENT_CONTENT);
- assertThat(systemdConfig).doesNotExist();
- }
- }
-
- @EnabledOnOs(OS.LINUX)
- @Test
- void uninstallChangingEtcEnvironmentFails() throws FatalInstallerError {
- install();
- assertThat(environmentFile.toFile().setWritable(false, false)).isTrue();
-
- Installer.UninstallerErrorReporter errorReporter = new Installer(sourceDirectory, targetDirectory,
- etcDirectory, false).runUninstall();
-
- assertThat(errorReporter.wereErrorsReported()).isTrue();
-
- assertThat(environmentFile).exists().content().contains("_JAVA_OPTIONS");
-
- // ensure that the agent uninstall step did not run because the preceding environment step failed
- assertThat(targetDirectory).exists();
- assertThat(installedTeamscaleProperties).exists();
- }
-
- @Test
- void noEtcEnvironment() throws FatalInstallerError, IOException {
- Files.delete(environmentFile);
- install();
-
- assertThat(environmentFile).doesNotExist();
- }
-
- @Test
- void noSystemd() throws FatalInstallerError, IOException {
- Files.delete(systemdDirectory);
- install();
-
- assertThat(systemdConfig).doesNotExist();
- }
-
- @Test
- void nonexistantTeamscaleUrl() {
- assertThatThrownBy(() -> install("http://does-not-exist:8080"))
- .hasMessageContaining("could not be resolved");
- assertThat(targetDirectory).doesNotExist();
- }
-
- @Test
- void connectionRefused() {
- assertThatThrownBy(() -> install("http://localhost:" + (TEAMSCALE_PORT + 1)))
- .hasMessageContaining("refused a connection");
- assertThat(targetDirectory).doesNotExist();
- }
-
- @Test
- void httpsInsteadOfHttp() {
- assertThatThrownBy(() -> install("https://localhost:" + TEAMSCALE_PORT))
- .hasMessageContaining("configured for HTTPS, not HTTP");
- assertThat(targetDirectory).doesNotExist();
- }
-
- @Test
- void profilerAlreadyInstalled() throws IOException {
- Files.createDirectories(targetDirectory);
- assertThatThrownBy(this::install).hasMessageContaining("Path already exists");
- }
-
- @Test
- void installDirectoryNotWritable() {
- assertThat(targetDirectory.getParent().toFile().setReadOnly()).isTrue();
- assertThatThrownBy(() -> install(TEAMSCALE_URL)).hasMessageContaining("Cannot create directory");
- }
-
- private void install() throws FatalInstallerError {
- install(TEAMSCALE_URL);
- }
-
- private void install(String teamscaleUrl) throws FatalInstallerError {
- new Installer(sourceDirectory, targetDirectory, etcDirectory, false).runInstall(
- new TeamscaleCredentials(HttpUrl.get(teamscaleUrl), "user", "accesskey"));
- }
-
-}
diff --git a/installer/src/test/java/com/teamscale/profiler/installer/LinuxInstallerTest.java b/installer/src/test/java/com/teamscale/profiler/installer/LinuxInstallerTest.java
new file mode 100644
index 000000000..7ff9b4538
--- /dev/null
+++ b/installer/src/test/java/com/teamscale/profiler/installer/LinuxInstallerTest.java
@@ -0,0 +1,181 @@
+package com.teamscale.profiler.installer;
+
+import com.teamscale.profiler.installer.utils.MockTeamscale;
+import com.teamscale.profiler.installer.utils.TestUtils;
+import com.teamscale.profiler.installer.windows.WindowsRegistry;
+import okhttp3.HttpUrl;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+import static com.teamscale.profiler.installer.utils.UninstallErrorReporterAssert.assertThat;
+import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledOnOs(OS.LINUX)
+class LinuxInstallerTest {
+
+ private static final int TEAMSCALE_PORT = 8059;
+ private static final String FILE_TO_INSTALL_CONTENT = "install-me";
+ private static final String NESTED_FILE_CONTENT = "nested-file";
+ private static final String ENVIRONMENT_CONTENT = "#this is /etc/environment\nPATH=/usr/bin";
+ private static final String TEAMSCALE_URL = "http://localhost:" + TEAMSCALE_PORT + "/";
+
+
+ private Path sourceDirectory;
+ private Path targetDirectory;
+ private Path etcDirectory;
+
+ private Path installedFile;
+ private Path installedTeamscaleProperties;
+ private Path installedAgentLibrary;
+
+ private Path environmentFile;
+ private Path systemdDirectory;
+ private Path systemdConfig;
+
+ private static MockTeamscale mockTeamscale;
+
+ @BeforeEach
+ void setUpSourceDirectory() throws IOException {
+ sourceDirectory = Files.createTempDirectory("InstallerTest-source");
+ targetDirectory = Files.createTempDirectory("InstallerTest-target").resolve("profiler");
+ etcDirectory = Files.createTempDirectory("InstallerTest-etc");
+
+ environmentFile = etcDirectory.resolve("environment");
+ Files.writeString(environmentFile, ENVIRONMENT_CONTENT);
+
+ systemdDirectory = etcDirectory.resolve("systemd");
+ Files.createDirectory(systemdDirectory);
+ systemdConfig = systemdDirectory.resolve("system.conf.d/teamscale-java-profiler.conf");
+
+ Path fileToInstall = sourceDirectory.resolve("install-me.txt");
+ Files.writeString(fileToInstall, FILE_TO_INSTALL_CONTENT, StandardOpenOption.CREATE);
+
+ Path nestedFileToInstall = sourceDirectory.resolve("lib/teamscale-jacoco-agent.jar");
+ Files.createDirectories(nestedFileToInstall.getParent());
+ Files.writeString(nestedFileToInstall, NESTED_FILE_CONTENT, StandardOpenOption.CREATE);
+
+ installedFile = targetDirectory.resolve(sourceDirectory.relativize(fileToInstall));
+ installedTeamscaleProperties = targetDirectory.resolve("teamscale.properties");
+ installedAgentLibrary = targetDirectory.resolve("lib/teamscale-jacoco-agent.jar");
+ }
+
+ @BeforeAll
+ static void startFakeTeamscale() {
+ mockTeamscale = new MockTeamscale(TEAMSCALE_PORT);
+ }
+
+ @AfterAll
+ static void stopFakeTeamscale() {
+ mockTeamscale.shutdown();
+ }
+
+ @Test
+ void successfulInstallation() throws FatalInstallerError, IOException {
+ install();
+
+ assertThat(Files.getPosixFilePermissions(installedTeamscaleProperties)).contains(OTHERS_READ);
+ assertThat(Files.getPosixFilePermissions(installedFile)).contains(OTHERS_READ);
+
+ assertThat(environmentFile).content().isEqualTo(ENVIRONMENT_CONTENT
+ + "\nJAVA_TOOL_OPTIONS=-javaagent:" + installedAgentLibrary
+ + "\n_JAVA_OPTIONS=-javaagent:" + installedAgentLibrary + "\n");
+
+ assertThat(systemdConfig).content().isEqualTo("[Manager]"
+ + "\nDefaultEnvironment=JAVA_TOOL_OPTIONS=-javaagent:" + installedAgentLibrary
+ + " _JAVA_OPTIONS=-javaagent:" + installedAgentLibrary + "\n");
+ }
+
+ @Test
+ void successfulUninstallation() throws FatalInstallerError {
+ install();
+ Installer.UninstallerErrorReporter errorReporter = uninstall();
+ assertThat(errorReporter).hadNoErrors();
+
+ assertThat(targetDirectory).doesNotExist();
+ assertThat(environmentFile).exists().content().isEqualTo(ENVIRONMENT_CONTENT);
+ assertThat(systemdConfig).doesNotExist();
+ }
+
+ @Test
+ void uninstallSuccessfullyEvenIfSystemDConfigWasManuallyRemoved() throws FatalInstallerError, IOException {
+ install();
+ Files.delete(systemdConfig);
+ Installer.UninstallerErrorReporter errorReporter = uninstall();
+ assertThat(errorReporter).hadNoErrors();
+ }
+
+ @Test
+ void uninstallSuccessfullyEvenIfEnvironmentFileDoesntExist() throws FatalInstallerError, IOException {
+ install();
+ Files.delete(environmentFile);
+ Installer.UninstallerErrorReporter errorReporter = uninstall();
+ assertThat(errorReporter).hadNoErrors();
+ }
+
+ @Test
+ void uninstallDeletingAgentDirectoryFails() throws Exception {
+ install();
+ TestUtils.makePathReadOnly(targetDirectory);
+ TestUtils.makePathReadOnly(installedTeamscaleProperties);
+
+ Installer.UninstallerErrorReporter errorReporter = uninstall();
+ assertThat(errorReporter).hadErrors();
+
+ assertThat(targetDirectory).exists();
+ assertThat(installedTeamscaleProperties).exists();
+ assertThat(environmentFile).exists().content().isEqualTo(ENVIRONMENT_CONTENT);
+ assertThat(systemdConfig).doesNotExist();
+ }
+
+ @Test
+ void uninstallChangingEtcEnvironmentFails() throws Exception {
+ install();
+ TestUtils.makePathReadOnly(environmentFile);
+
+ Installer.UninstallerErrorReporter errorReporter = uninstall();
+ assertThat(errorReporter).hadErrors();
+
+ assertThat(environmentFile).exists().content().contains("_JAVA_OPTIONS");
+
+ // ensure that the agent uninstall step did not run because the preceding environment step failed
+ assertThat(targetDirectory).exists();
+ assertThat(installedTeamscaleProperties).exists();
+ }
+
+ @Test
+ void noEtcEnvironment() throws FatalInstallerError, IOException {
+ Files.delete(environmentFile);
+ install();
+
+ assertThat(environmentFile).doesNotExist();
+ }
+
+ @Test
+ void noSystemd() throws FatalInstallerError, IOException {
+ Files.delete(systemdDirectory);
+ install();
+
+ assertThat(systemdConfig).doesNotExist();
+ }
+
+ private void install() throws FatalInstallerError {
+ new Installer(sourceDirectory, targetDirectory, etcDirectory, false, WindowsRegistry.INSTANCE).runInstall(
+ new TeamscaleCredentials(HttpUrl.get(LinuxInstallerTest.TEAMSCALE_URL), "user", "accesskey"));
+ }
+
+ private Installer.UninstallerErrorReporter uninstall() {
+ return new Installer(sourceDirectory, targetDirectory,
+ etcDirectory, false, WindowsRegistry.INSTANCE).runUninstall();
+ }
+
+}
diff --git a/installer/src/test/java/com/teamscale/profiler/installer/WindowsInstallerTest.java b/installer/src/test/java/com/teamscale/profiler/installer/WindowsInstallerTest.java
new file mode 100644
index 000000000..361b61c5d
--- /dev/null
+++ b/installer/src/test/java/com/teamscale/profiler/installer/WindowsInstallerTest.java
@@ -0,0 +1,96 @@
+package com.teamscale.profiler.installer;
+
+import com.teamscale.profiler.installer.utils.MockRegistry;
+import com.teamscale.profiler.installer.utils.MockTeamscale;
+import com.teamscale.profiler.installer.utils.UninstallErrorReporterAssert;
+import okhttp3.HttpUrl;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledOnOs(OS.WINDOWS)
+class WindowsInstallerTest {
+
+ private static final int TEAMSCALE_PORT = 8059;
+ private static final String FILE_TO_INSTALL_CONTENT = "install-me";
+ private static final String NESTED_FILE_CONTENT = "nested-file";
+ private static final String TEAMSCALE_URL = "http://localhost:" + TEAMSCALE_PORT + "/";
+
+
+ private Path sourceDirectory;
+ private Path targetDirectory;
+
+ private Path installedAgentLibrary;
+
+ private static MockTeamscale mockTeamscale;
+
+ private MockRegistry registry;
+
+ @BeforeEach
+ void setUpSourceDirectory() throws IOException {
+ sourceDirectory = Files.createTempDirectory("InstallerTest-source");
+ targetDirectory = Files.createTempDirectory("InstallerTest-target").resolve("profiler");
+
+ Path fileToInstall = sourceDirectory.resolve("install-me.txt");
+ Files.writeString(fileToInstall, FILE_TO_INSTALL_CONTENT, StandardOpenOption.CREATE);
+
+ Path nestedFileToInstall = sourceDirectory.resolve("lib/teamscale-jacoco-agent.jar");
+ Files.createDirectories(nestedFileToInstall.getParent());
+ Files.writeString(nestedFileToInstall, NESTED_FILE_CONTENT,
+ StandardOpenOption.CREATE);
+
+ installedAgentLibrary = targetDirectory.resolve("lib/teamscale-jacoco-agent.jar");
+
+ registry = new MockRegistry();
+ }
+
+ @BeforeAll
+ static void startFakeTeamscale() {
+ mockTeamscale = new MockTeamscale(TEAMSCALE_PORT);
+ }
+
+ @AfterAll
+ static void stopFakeTeamscale() {
+ mockTeamscale.shutdown();
+ }
+
+ @Test
+ void successfulInstallation() throws FatalInstallerError {
+ install();
+
+ assertThat(registry.getVariable("_JAVA_OPTIONS")).isEqualTo("-javaagent:" + installedAgentLibrary);
+ assertThat(registry.getVariable("JAVA_TOOL_OPTIONS")).isEqualTo("-javaagent:" + installedAgentLibrary);
+ }
+
+ @Test
+ void successfulUninstallation() throws FatalInstallerError {
+ install();
+ Installer.UninstallerErrorReporter errorReporter = uninstall();
+ UninstallErrorReporterAssert.assertThat(errorReporter).hadNoErrors();
+
+ assertThat(registry.getVariable("_JAVA_OPTIONS")).isNull();
+ assertThat(registry.getVariable("JAVA_TOOL_OPTIONS")).isNull();
+ }
+
+ private void install() throws FatalInstallerError {
+ new Installer(sourceDirectory, targetDirectory, Paths.get("/etc"), false, registry).runInstall(
+ new TeamscaleCredentials(HttpUrl.get(WindowsInstallerTest.TEAMSCALE_URL), "user", "accesskey"));
+ }
+
+ private Installer.UninstallerErrorReporter uninstall() {
+ return new Installer(sourceDirectory, targetDirectory,
+ Paths.get("/etc"), false, registry).runUninstall();
+ }
+
+}
diff --git a/installer/src/test/java/com/teamscale/profiler/installer/steps/InstallWindowsSystemEnvironmentStepTest.java b/installer/src/test/java/com/teamscale/profiler/installer/steps/InstallWindowsSystemEnvironmentStepTest.java
new file mode 100644
index 000000000..889f3cb92
--- /dev/null
+++ b/installer/src/test/java/com/teamscale/profiler/installer/steps/InstallWindowsSystemEnvironmentStepTest.java
@@ -0,0 +1,74 @@
+package com.teamscale.profiler.installer.steps;
+
+import com.teamscale.profiler.installer.JvmEnvironmentMap;
+import com.teamscale.profiler.installer.FatalInstallerError;
+import com.teamscale.profiler.installer.Installer;
+import com.teamscale.profiler.installer.utils.MockRegistry;
+import com.teamscale.profiler.installer.TeamscaleCredentials;
+import okhttp3.HttpUrl;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests the installation step that sets Windows environment variables. This test mocks the registry so we can test this
+ * step on any OS.
+ */
+class InstallWindowsSystemEnvironmentStepTest {
+
+ private static final TeamscaleCredentials CREDENTIALS = new TeamscaleCredentials(
+ HttpUrl.get("http://localhost:" + 8058 + "/"), "user", "accesskey");
+
+ private static final JvmEnvironmentMap ENVIRONMENT = new JvmEnvironmentMap(
+ "_JAVA_OPTIONS", "-javaagent:C:\\Program Files\\foo.jar",
+ "JAVA_TOOL_OPTIONS", "-javaagent:C:\\Programs\\foo.jar");
+
+ @Test
+ void successfulInstall() throws FatalInstallerError {
+ MockRegistry registry = new MockRegistry();
+ new InstallWindowsSystemEnvironmentStep(ENVIRONMENT, registry).install(CREDENTIALS);
+
+ assertThat(registry.getVariable("_JAVA_OPTIONS")).isEqualTo("\"-javaagent:C:\\Program Files\\foo.jar\"");
+ assertThat(registry.getVariable("JAVA_TOOL_OPTIONS")).isEqualTo("-javaagent:C:\\Programs\\foo.jar");
+ }
+
+ @Test
+ void successfulUninstall() throws FatalInstallerError {
+ MockRegistry registry = new MockRegistry();
+ Installer.UninstallerErrorReporter errorReporter = new Installer.UninstallerErrorReporter();
+
+ new InstallWindowsSystemEnvironmentStep(ENVIRONMENT, registry).install(CREDENTIALS);
+ new InstallWindowsSystemEnvironmentStep(ENVIRONMENT, registry).uninstall(errorReporter);
+
+ assertThat(errorReporter.wereErrorsReported()).isFalse();
+ assertThat(registry.getVariable("_JAVA_OPTIONS")).isNull();
+ }
+
+ @Test
+ void addAndRemoveProfiler() throws FatalInstallerError {
+ MockRegistry registry = new MockRegistry();
+
+ InstallWindowsSystemEnvironmentStep.addProfiler("_JAVA_OPTIONS", "-javaagent:foo.jar", registry);
+ assertThat(registry.getVariable("_JAVA_OPTIONS")).isEqualTo("-javaagent:foo.jar");
+
+ InstallWindowsSystemEnvironmentStep.removeProfiler("_JAVA_OPTIONS", "-javaagent:foo.jar", registry);
+ assertThat(registry.getVariable("_JAVA_OPTIONS")).isNullOrEmpty();
+ }
+
+ @Test
+ void addAndRemoveProfilerWithPreviousValue() throws FatalInstallerError {
+ MockRegistry registry = new MockRegistry();
+
+ registry.setVariable("_JAVA_OPTIONS", "-javaagent:other.jar");
+ InstallWindowsSystemEnvironmentStep.addProfiler("_JAVA_OPTIONS", "-javaagent:foo.jar", registry);
+ assertThat(registry.getVariable("_JAVA_OPTIONS")).isEqualTo("-javaagent:foo.jar -javaagent:other.jar");
+
+ InstallWindowsSystemEnvironmentStep.removeProfiler("_JAVA_OPTIONS", "-javaagent:foo.jar", registry);
+ assertThat(registry.getVariable("_JAVA_OPTIONS")).isEqualTo("-javaagent:other.jar");
+
+ // removing it again should do nothing
+ InstallWindowsSystemEnvironmentStep.removeProfiler("_JAVA_OPTIONS", "-javaagent:foo.jar", registry);
+ assertThat(registry.getVariable("_JAVA_OPTIONS")).isEqualTo("-javaagent:other.jar");
+ }
+
+}
\ No newline at end of file
diff --git a/installer/src/test/java/com/teamscale/profiler/installer/utils/MockRegistry.java b/installer/src/test/java/com/teamscale/profiler/installer/utils/MockRegistry.java
new file mode 100644
index 000000000..43c6a4f8a
--- /dev/null
+++ b/installer/src/test/java/com/teamscale/profiler/installer/utils/MockRegistry.java
@@ -0,0 +1,40 @@
+package com.teamscale.profiler.installer.utils;
+
+import com.teamscale.profiler.installer.windows.IRegistry;
+import com.teamscale.profiler.installer.windows.WindowsRegistry;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Mock of {@link IRegistry} to allow tests without actually changing the Windows registry.
+ */
+public class MockRegistry implements IRegistry {
+
+ private final Map values = new HashMap<>();
+
+ @Override
+ public String getHklmValue(String key, String name) {
+ return values.get(key + "\\" + name);
+ }
+
+ @Override
+ public void setHklmValue(String key, String name, String value) {
+ values.put(key + "\\" + name, value);
+ }
+
+ @Override
+ public void deleteHklmValue(String key, String name) {
+ values.remove(key + "\\" + name);
+ }
+
+ /** Reads the given environment variable from the registry. */
+ public String getVariable(String name) {
+ return getHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, name);
+ }
+
+ /** Sets the given environment variable in the registry. */
+ public void setVariable(String name, String value) {
+ setHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, name, value);
+ }
+}
diff --git a/installer/src/test/java/com/teamscale/profiler/installer/MockTeamscale.java b/installer/src/test/java/com/teamscale/profiler/installer/utils/MockTeamscale.java
similarity index 75%
rename from installer/src/test/java/com/teamscale/profiler/installer/MockTeamscale.java
rename to installer/src/test/java/com/teamscale/profiler/installer/utils/MockTeamscale.java
index 9f793df19..fc7d7d7a4 100644
--- a/installer/src/test/java/com/teamscale/profiler/installer/MockTeamscale.java
+++ b/installer/src/test/java/com/teamscale/profiler/installer/utils/MockTeamscale.java
@@ -1,4 +1,4 @@
-package com.teamscale.profiler.installer;
+package com.teamscale.profiler.installer.utils;
import spark.Request;
import spark.Response;
@@ -8,17 +8,20 @@
import static javax.servlet.http.HttpServletResponse.SC_OK;
/**
- * Mocks a redirect server: redirects all requests to another server.
+ * Mocks Teamscale. Returns a fixed status code for all requests.
+ * By default: status 200
*/
public class MockTeamscale {
private final Service service;
+ private int statusCode = SC_OK;
+
public MockTeamscale(int port) {
service = Service.ignite();
service.port(port);
service.get("/*", (Request request, Response response) -> {
- response.status(SC_OK);
+ response.status(statusCode);
response.body("fake content");
return response;
});
@@ -29,6 +32,9 @@ public MockTeamscale(int port) {
service.awaitInitialization();
}
+ public void setStatusCode(int statusCode) {
+ this.statusCode = statusCode;
+ }
/**
* Shuts down the mock server and waits for it to be stopped.
diff --git a/installer/src/test/java/com/teamscale/profiler/installer/utils/TeamscaleUtilsTest.java b/installer/src/test/java/com/teamscale/profiler/installer/utils/TeamscaleUtilsTest.java
new file mode 100644
index 000000000..de5851977
--- /dev/null
+++ b/installer/src/test/java/com/teamscale/profiler/installer/utils/TeamscaleUtilsTest.java
@@ -0,0 +1,52 @@
+package com.teamscale.profiler.installer.utils;
+
+import com.teamscale.profiler.installer.FatalInstallerError;
+import com.teamscale.profiler.installer.TeamscaleCredentials;
+import okhttp3.HttpUrl;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class TeamscaleUtilsTest {
+
+ private static MockTeamscale mockTeamscale;
+ private static final int TEAMSCALE_PORT = 8758;
+
+ @BeforeAll
+ static void startMockTeamscale() {
+ mockTeamscale = new MockTeamscale(TEAMSCALE_PORT);
+ }
+
+ @AfterAll
+ static void stopMockTeamscale() {
+ mockTeamscale.shutdown();
+ }
+
+ @Test
+ void unresolvableUrl() {
+ Assertions.assertThatThrownBy(() ->
+ checkTeamscaleConnection("http://totally.invalid:9999")
+ ).hasMessageContaining("The host http://totally.invalid:9999/ could not be resolved");
+ }
+
+ @Test
+ void unreachableUrl() {
+ Assertions.assertThatThrownBy(() ->
+ checkTeamscaleConnection("http://localhost:9999")
+ ).hasMessageContaining("The host http://localhost:9999/ refused a connection");
+ }
+
+ @Test
+ void incorrectCredentials() {
+ mockTeamscale.setStatusCode(401);
+ Assertions.assertThatThrownBy(() ->
+ checkTeamscaleConnection("http://localhost:" + TEAMSCALE_PORT)
+ ).hasMessageContaining("You provided incorrect credentials");
+ }
+
+ private void checkTeamscaleConnection(String url) throws FatalInstallerError {
+ TeamscaleUtils.checkTeamscaleConnection(new TeamscaleCredentials(HttpUrl.get(url), "user", "key"));
+ }
+
+}
\ No newline at end of file
diff --git a/installer/src/test/java/com/teamscale/profiler/installer/utils/TestUtils.java b/installer/src/test/java/com/teamscale/profiler/installer/utils/TestUtils.java
new file mode 100644
index 000000000..23377a8d5
--- /dev/null
+++ b/installer/src/test/java/com/teamscale/profiler/installer/utils/TestUtils.java
@@ -0,0 +1,58 @@
+package com.teamscale.profiler.installer.utils;
+
+import org.apache.commons.lang3.SystemUtils;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.AclEntry;
+import java.nio.file.attribute.AclEntryFlag;
+import java.nio.file.attribute.AclEntryPermission;
+import java.nio.file.attribute.AclEntryType;
+import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.DosFileAttributeView;
+import java.nio.file.attribute.UserPrincipal;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Generic utilities for tests. */
+public class TestUtils {
+
+ private static final EnumSet READ_ONLY_DENIED_PERMISSIONS = EnumSet.of(
+ AclEntryPermission.WRITE_DATA, AclEntryPermission.DELETE, AclEntryPermission.DELETE_CHILD,
+ AclEntryPermission.ADD_FILE, AclEntryPermission.ADD_SUBDIRECTORY, AclEntryPermission.APPEND_DATA,
+ AclEntryPermission.WRITE_ATTRIBUTES);
+
+ /**
+ * Changes the given path to read-only and asserts that the change was successful. Depending on the operating
+ * system, this may or may not also mark subpaths as read-only, so do not rely on this.
+ */
+ public static void makePathReadOnly(Path path) throws IOException {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ // File#setWritable doesn't work under Windows 11 (always returns false).
+ // So we manually set the readonly attribute and some ACLs. Unlike under Linux, this only prevent deletion
+ // of this specific path, not its subpaths.
+ // Adapted from https://stackoverflow.com/a/25747561/1396068
+ DosFileAttributeView dosAttributes = Files.getFileAttributeView(path, DosFileAttributeView.class);
+ dosAttributes.setReadOnly(true);
+
+ AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class);
+ UserPrincipal owner = view.getOwner();
+
+ List newAcl = new ArrayList<>(view.getAcl());
+ newAcl.add(0, AclEntry.newBuilder()
+ .setType(AclEntryType.DENY)
+ .setPrincipal(owner)
+ .setPermissions(READ_ONLY_DENIED_PERMISSIONS)
+ .setFlags(AclEntryFlag.FILE_INHERIT, AclEntryFlag.DIRECTORY_INHERIT)
+ .build());
+ view.setAcl(newAcl);
+ } else {
+ assertThat(path.toFile().setWritable(false, false))
+ .withFailMessage("Failed to mark " + path + " as writable = " + false).isTrue();
+ }
+ }
+}
diff --git a/installer/src/test/java/com/teamscale/profiler/installer/utils/UninstallErrorReporterAssert.java b/installer/src/test/java/com/teamscale/profiler/installer/utils/UninstallErrorReporterAssert.java
new file mode 100644
index 000000000..4016d1112
--- /dev/null
+++ b/installer/src/test/java/com/teamscale/profiler/installer/utils/UninstallErrorReporterAssert.java
@@ -0,0 +1,32 @@
+package com.teamscale.profiler.installer.utils;
+
+import com.teamscale.profiler.installer.Installer;
+import org.assertj.core.api.AbstractAssert;
+
+/** Assertions for {@link com.teamscale.profiler.installer.Installer.UninstallerErrorReporter} */
+public class UninstallErrorReporterAssert extends AbstractAssert {
+
+ protected UninstallErrorReporterAssert(Installer.UninstallerErrorReporter uninstallerErrorReporter,
+ Class> selfType) {
+ super(uninstallerErrorReporter, selfType);
+ }
+
+ /** Asserts that no errors were reported. */
+ public void hadNoErrors() {
+ if (actual.wereErrorsReported()) {
+ failWithMessage("Expected no errors to be reported during the uninstallation, but at least one occurred.");
+ }
+ }
+
+ /** Asserts at least one error was reported. */
+ public void hadErrors() {
+ if (!actual.wereErrorsReported()) {
+ failWithMessage("Unexpectedly, no errors were reported during the uninstallation.");
+ }
+ }
+
+ /** Creates an assert for the given reporter. */
+ public static UninstallErrorReporterAssert assertThat(Installer.UninstallerErrorReporter reporter) {
+ return new UninstallErrorReporterAssert(reporter, UninstallErrorReporterAssert.class);
+ }
+}
diff --git a/installer/src/test/java/com/teamscale/profiler/installer/windows/WindowsRegistryTest.java b/installer/src/test/java/com/teamscale/profiler/installer/windows/WindowsRegistryTest.java
new file mode 100644
index 000000000..675cdfc93
--- /dev/null
+++ b/installer/src/test/java/com/teamscale/profiler/installer/windows/WindowsRegistryTest.java
@@ -0,0 +1,40 @@
+package com.teamscale.profiler.installer.windows;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests our registry access class.
+ */
+@EnabledOnOs(OS.WINDOWS)
+class WindowsRegistryTest {
+
+ private static final String VARIABLE = "TEAMSCALE_JAVA_PROFILER_WINDOWS_REGISTRY_TEST";
+
+ @BeforeEach
+ void clearRegistry() throws Exception {
+ Runtime.getRuntime()
+ .exec(new String[]{"reg", "delete", "HKLM\\" + WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, "/v", VARIABLE});
+ }
+
+ @Test
+ void testAllFunctions() throws Exception {
+ assertThat(WindowsRegistry.INSTANCE.getHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, VARIABLE)).isNull();
+
+ WindowsRegistry.INSTANCE.setHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, VARIABLE, "foobar");
+ assertThat(WindowsRegistry.INSTANCE.getHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, VARIABLE)).isEqualTo(
+ "foobar");
+
+ WindowsRegistry.INSTANCE.setHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, VARIABLE, "goo");
+ assertThat(WindowsRegistry.INSTANCE.getHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, VARIABLE)).isEqualTo(
+ "goo");
+
+ WindowsRegistry.INSTANCE.deleteHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, VARIABLE);
+ assertThat(WindowsRegistry.INSTANCE.getHklmValue(WindowsRegistry.ENVIRONMENT_REGISTRY_KEY, VARIABLE)).isNull();
+ }
+
+}
\ No newline at end of file
diff --git a/system-tests/maven-external-upload-test/src/test/java/com/teamscale/upload/MavenExternalUploadSystemTest.java b/system-tests/maven-external-upload-test/src/test/java/com/teamscale/upload/MavenExternalUploadSystemTest.java
index 17c6538f6..a0084698c 100644
--- a/system-tests/maven-external-upload-test/src/test/java/com/teamscale/upload/MavenExternalUploadSystemTest.java
+++ b/system-tests/maven-external-upload-test/src/test/java/com/teamscale/upload/MavenExternalUploadSystemTest.java
@@ -3,8 +3,8 @@
import com.teamscale.test.commons.ExternalReport;
import com.teamscale.test.commons.SystemTestUtils;
import com.teamscale.test.commons.TeamscaleMockServer;
+import org.apache.commons.lang3.SystemUtils;
import org.conqat.lib.commons.io.ProcessUtils;
-import org.conqat.lib.commons.system.SystemUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -17,8 +17,8 @@
import static org.assertj.core.api.Assertions.fail;
/**
- * Runs several Maven projects' Surefire and Failsafe tests that produce coverage via the Jacoco Maven plugin.
- * Checks that the Jacoco reports are correctly uploaded to a Teamscale instance.
+ * Runs several Maven projects' Surefire and Failsafe tests that produce coverage via the Jacoco Maven plugin. Checks
+ * that the Jacoco reports are correctly uploaded to a Teamscale instance.
*/
public class MavenExternalUploadSystemTest {
@@ -41,13 +41,13 @@ public void startFakeTeamscaleServer() throws Exception {
private ProcessUtils.ExecutionResult runCoverageUploadGoal(String projectPath) {
File workingDirectory = new File(projectPath);
String executable = "./mvnw";
- if (SystemUtils.isWindows()) {
+ if (SystemUtils.IS_OS_WINDOWS) {
executable = Paths.get(projectPath, "mvnw.cmd").toUri().getPath();
}
try {
- return ProcessUtils.execute(new ProcessBuilder(executable , MAVEN_COVERAGE_UPLOAD_GOAL).directory(workingDirectory));
- }
- catch (IOException e) {
+ return ProcessUtils.execute(
+ new ProcessBuilder(executable, MAVEN_COVERAGE_UPLOAD_GOAL).directory(workingDirectory));
+ } catch (IOException e) {
fail(String.valueOf(e));
}
return null;
@@ -70,8 +70,9 @@ public void testIncorrectJaCoCoConfiguration() throws IOException {
ProcessUtils.ExecutionResult result = runCoverageUploadGoal(FAILING_MAVEN_PROJECT_NAME);
assertThat(result).isNotNull();
assertThat(teamscaleMockServer.uploadedReports.size()).isEqualTo(0);
- assertThat(result.getStdout()).contains(String.format("Skipping upload for %s as %s is not configured to produce XML reports",
- FAILING_MAVEN_PROJECT_NAME, "org.jacoco:jacoco-maven-plugin"));
+ assertThat(result.getStdout()).contains(
+ String.format("Skipping upload for %s as %s is not configured to produce XML reports",
+ FAILING_MAVEN_PROJECT_NAME, "org.jacoco:jacoco-maven-plugin"));
}
@AfterAll
diff --git a/system-tests/tia-maven/src/test/java/com/teamscale/tia/TiaMavenMultipleJobsTest.java b/system-tests/tia-maven/src/test/java/com/teamscale/tia/TiaMavenMultipleJobsTest.java
index b14d9a7ac..bc98d2d5a 100644
--- a/system-tests/tia-maven/src/test/java/com/teamscale/tia/TiaMavenMultipleJobsTest.java
+++ b/system-tests/tia-maven/src/test/java/com/teamscale/tia/TiaMavenMultipleJobsTest.java
@@ -1,11 +1,10 @@
package com.teamscale.tia;
+import com.teamscale.test.commons.SystemTestUtils;
import org.conqat.lib.commons.filesystem.FileSystemUtils;
-import org.conqat.lib.commons.io.ProcessUtils;
-import org.conqat.lib.commons.system.SystemUtils;
import org.junit.jupiter.api.Test;
-import java.io.File;
+import java.nio.file.Path;
import java.nio.file.Paths;
import static org.assertj.core.api.Assertions.assertThat;
@@ -16,32 +15,27 @@
public class TiaMavenMultipleJobsTest {
/**
- * Starts multiple maven processes and checks that the ports are dynamically set and the servers are correctly
+ * Starts multiple Maven processes and checks that the ports are dynamically set and the servers are correctly
* started.
*/
@Test
public void testMavenTia() throws Exception {
- File workingDirectory = new File("maven-dump-local-project");
- String executable = "./mvnw";
- if (SystemUtils.isWindows()) {
- executable = Paths.get("maven-dump-local-project", "mvnw.cmd").toUri().getPath();
- }
+ String workingDirectory = "maven-dump-local-project";
+
// Clean once before testing parallel execution and make sure that the cleaning
// process is finished before testing.
- ProcessUtils.ExecutionResult result = ProcessUtils
- .execute(new ProcessBuilder(executable, "clean").directory(workingDirectory));
- assertThat(result.terminatedByTimeoutOrInterruption()).isFalse();
+ SystemTestUtils.runMaven(workingDirectory, "clean");
+
+ // run three verify processes in parallel without waiting
for (int i = 0; i < 3; i++) {
- new ProcessBuilder(executable, "verify").directory(workingDirectory).start();
+ SystemTestUtils.buildMavenProcess(workingDirectory, "verify").start();
}
- result = ProcessUtils
- .execute(new ProcessBuilder(executable, "verify").directory(workingDirectory));
- // Get additional output for error debugging.
- System.out.println(result.getStdout());
- File configFile = new File(Paths.get(workingDirectory.getAbsolutePath(), "target", "tia", "agent.log").toUri());
- String configContent = FileSystemUtils.readFile(configFile);
- assertThat(configContent).isNotEmpty();
- assertThat(result.terminatedByTimeoutOrInterruption()).isFalse();
- assertThat(configContent).doesNotContain("Could not start http server on port");
+
+ // and one more that we wait for to terminate
+ SystemTestUtils.runMaven(workingDirectory, "verify");
+
+ Path configFile = Paths.get(workingDirectory, "target", "tia", "agent.log");
+ String configContent = FileSystemUtils.readFile(configFile.toFile());
+ assertThat(configContent).isNotEmpty().doesNotContain("Could not start http server on port");
}
}
diff --git a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TestwiseCoverageReportAssert.kt b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TestwiseCoverageReportAssert.kt
index 9b4d00912..a99110526 100644
--- a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TestwiseCoverageReportAssert.kt
+++ b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TestwiseCoverageReportAssert.kt
@@ -7,95 +7,107 @@ import org.assertj.core.api.AbstractAssert
import java.io.File
class TestwiseCoverageReportAssert(actual: TestwiseCoverageReport) :
- AbstractAssert(
- actual,
- TestwiseCoverageReportAssert::class.java
- ) {
-
- /** Asserts that the report has the partial flag set to the given value. */
- fun hasPartial(expectedPartial: Boolean): TestwiseCoverageReportAssert {
- isNotNull
-
- if (actual.partial != expectedPartial) {
- failWithMessage(
- "Expected partial flag to be <%s> but was <%s>",
- expectedPartial,
- actual.partial
- )
- }
-
- return this
- }
-
- fun containsExecutionResult(testUniformPath: String, result: ETestExecutionResult): TestwiseCoverageReportAssert {
- isNotNull
-
- val test = getTest(testUniformPath)
- if (test.result != result) {
- failWithMessage(
- "Expected test execution result of %s to be <%s> but was <%s>",
- testUniformPath,
- result,
- test.result
- )
- }
-
- return this
- }
-
- private fun getTest(test: String): TestInfo {
- return actual.tests.single { it.uniformPath == test }
- }
-
- /** Asserts that the given number of tests have non-empty coverage */
- fun hasTestsWithCoverage(size: Int): TestwiseCoverageReportAssert {
- isNotNull
-
- if (actual.tests.count { it.paths.isNotEmpty() } != size) {
- failWithMessage("Expected <%s> test(s) with coverage but got <%s>", size, actual.tests.size)
- }
-
- return this
- }
-
- fun hasSize(size: Int): TestwiseCoverageReportAssert {
- isNotNull
-
- if (actual.tests.size != size) {
- failWithMessage("Expected <%s> test(s) but got <%s>", size, actual.tests.size)
- }
-
- return this
- }
-
- fun containsCoverage(
- testUniformPath: String,
- filePath: String,
- expectedCoveredLines: String
- ): TestwiseCoverageReportAssert {
- isNotNull
-
- val path = File(filePath).parent
- val fileName = File(filePath).name
-
- val test = getTest(testUniformPath)
- val actualCoveredLines =
- test.paths.single { it.path == path }.files.single { it.fileName == fileName }.coveredLines
- if (actualCoveredLines != expectedCoveredLines) {
- failWithMessage(
- "Expected %s to cover <%s> but was <%s>",
- testUniformPath,
- expectedCoveredLines,
- actualCoveredLines
- )
- }
-
- return this
- }
-
- companion object {
- fun assertThat(actual: TestwiseCoverageReport): TestwiseCoverageReportAssert {
- return TestwiseCoverageReportAssert(actual)
- }
- }
+ AbstractAssert(
+ actual,
+ TestwiseCoverageReportAssert::class.java
+ ) {
+
+ /** Asserts that the report has the partial flag set to the given value. */
+ fun hasPartial(expectedPartial: Boolean): TestwiseCoverageReportAssert {
+ isNotNull
+
+ if (actual.partial != expectedPartial) {
+ failWithMessage(
+ "Expected partial flag to be <%s> but was <%s>",
+ expectedPartial,
+ actual.partial
+ )
+ }
+
+ return this
+ }
+
+ fun containsExecutionResult(testUniformPath: String, result: ETestExecutionResult): TestwiseCoverageReportAssert {
+ isNotNull
+
+ val test = getTest(testUniformPath)
+ if (test.result != result) {
+ failWithMessage(
+ "Expected test execution result of %s to be <%s> but was <%s>",
+ testUniformPath,
+ result,
+ test.result
+ )
+ }
+
+ return this
+ }
+
+ private fun getTest(test: String): TestInfo {
+ return actual.tests.single { it.uniformPath == test }
+ }
+
+ /** Asserts that the given number of tests have non-empty coverage */
+ fun hasTestsWithCoverage(size: Int): TestwiseCoverageReportAssert {
+ isNotNull
+
+ if (actual.tests.count { it.paths.isNotEmpty() } != size) {
+ failWithMessage("Expected <%s> test(s) with coverage but got <%s>", size, actual.tests.size)
+ }
+
+ return this
+ }
+
+ fun hasSize(size: Int): TestwiseCoverageReportAssert {
+ isNotNull
+
+ if (actual.tests.size != size) {
+ failWithMessage("Expected <%s> test(s) but got <%s>", size, actual.tests.size)
+ }
+
+ return this
+ }
+
+ fun containsCoverage(
+ testUniformPath: String,
+ filePath: String,
+ expectedCoveredLines: String
+ ): TestwiseCoverageReportAssert {
+ isNotNull
+
+ // testwise reports always use slashes, File uses backslashes on Windows
+ val path = File(filePath).parent.replace('\\', '/')
+ val fileName = File(filePath).name
+
+ val test = getTest(testUniformPath)
+ val pathCoverage = test.paths.find { it.path == path }
+ if (pathCoverage == null) {
+ failWithMessage("Expected %s to cover path %s but it did not", testUniformPath, path)
+ return this
+ }
+
+ val fileCoverage = pathCoverage.files.find { it.fileName == fileName }
+ if (fileCoverage == null) {
+ failWithMessage("Expected %s to cover file %s but it did not", testUniformPath, filePath)
+ return this
+ }
+
+ val actualCoveredLines = fileCoverage.coveredLines
+ if (actualCoveredLines != expectedCoveredLines) {
+ failWithMessage(
+ "Expected %s to cover <%s> but was <%s>",
+ testUniformPath,
+ expectedCoveredLines,
+ actualCoveredLines
+ )
+ }
+
+ return this
+ }
+
+ companion object {
+ fun assertThat(actual: TestwiseCoverageReport): TestwiseCoverageReportAssert {
+ return TestwiseCoverageReportAssert(actual)
+ }
+ }
}
\ No newline at end of file
diff --git a/teamscale-maven-plugin/build.gradle.kts b/teamscale-maven-plugin/build.gradle.kts
index 1f68a0604..2d5fbc5b7 100644
--- a/teamscale-maven-plugin/build.gradle.kts
+++ b/teamscale-maven-plugin/build.gradle.kts
@@ -3,8 +3,11 @@ import org.codehaus.plexus.util.Os
abstract class MavenExec : Exec() {
@TaskAction
override fun exec() {
- val mavenExecutable = if (Os.isFamily(Os.FAMILY_WINDOWS)) "mvnw.cmd" else "./mvnw"
- executable = mavenExecutable
+ executable = "./mvnw"
+ if (Os.isFamily(Os.FAMILY_WINDOWS)) {
+ executable = "cmd"
+ args(listOf("/c", "mvnw.cmd") + (args ?: emptyList()))
+ }
workingDir(".")
project.file("pom.xml").writeText(
project.file("pom.xml").readText().replace(