Allow re-using server when running tests with the new framework

Closes #44101

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
stianst 2026-01-14 11:13:52 +01:00 committed by Pedro Igor
parent 4f91b5246e
commit 8aaf3e4606
12 changed files with 576 additions and 134 deletions

View file

@ -101,10 +101,11 @@ Valid values:
Configuration:
| Value | Description |
|---------------------------------------------------|----------------------------------------------------------------------------------------|
| `kc.test.server.config` / `KC_TEST_SERVER_CONFIG` | The name of a KeycloakServerConfig class to use when running the tests |
| `kc.test.server.kcw` / `KC_TEST_SERVER_KCW` | Set to a kcw command to use kcw with remote server (see `kcw help` for valid commands) |
| Value | Description |
|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| `kc.test.server.reuse` / `KC_TEST_SERVER_REUSE` | If enabled for the `distribution` server the started server will be left running and re-used for subsequent runs |
| `kc.test.server.config` / `KC_TEST_SERVER_CONFIG` | The name of a KeycloakServerConfig class to use when running the tests |
| `kc.test.server.kcw` / `KC_TEST_SERVER_KCW` | Set to a kcw command to use kcw with remote server (see `kcw help` for valid commands) |
### Database

View file

@ -131,10 +131,6 @@ public class ClusteredKeycloakServer implements KeycloakServer {
for (var dependency : configBuilder.toDependencies()) {
container.copyProvider(dependency.getGroupId(), dependency.getArtifactId());
}
for(var config : configBuilder.toConfigFiles()) {
container.copyConfigFile(config);
}
}
@Override

View file

@ -1,9 +1,22 @@
package org.keycloak.testframework.database;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Map;
import org.keycloak.testframework.util.TmpDir;
import org.apache.commons.io.FileUtils;
import org.eclipse.microprofile.config.inject.ConfigProperty;
public class DevFileDatabaseSupplier extends AbstractDatabaseSupplier {
private static final File DB_DIR = Path.of(TmpDir.resolveTmpDir().getAbsolutePath(), "kc-test-framework", "h2").toFile();
@ConfigProperty(name = "reuse", defaultValue = "false")
boolean reuse;
@Override
public String getAlias() {
return "dev-file";
@ -11,25 +24,46 @@ public class DevFileDatabaseSupplier extends AbstractDatabaseSupplier {
@Override
TestDatabase getTestDatabase() {
return new DevFileTestDatabase();
return new DevFileTestDatabase(reuse);
}
private static class DevFileTestDatabase implements TestDatabase {
private final boolean reuse;
public DevFileTestDatabase(boolean reuse) {
this.reuse = reuse;
}
@Override
public void start(DatabaseConfiguration config) {
if (config.getInitScript() != null)
deleteDatabase();
if (config.getInitScript() != null) {
throw new IllegalArgumentException("init script not supported, configure h2 properties via --db-url-properties");
}
}
@Override
public void stop() {
// TODO Should we clean-up H2 database here?
deleteDatabase();
}
private void deleteDatabase() {
if (!reuse && DB_DIR.exists()) {
try {
FileUtils.deleteDirectory(DB_DIR);
} catch (IOException e) {
throw new RuntimeException("Failed to delete directory: " + DB_DIR.getAbsolutePath(), e);
}
}
}
@Override
public Map<String, String> serverConfig() {
return Map.of("db", "dev-file");
return Map.of(
"db", "dev-file",
"db-url", "jdbc:h2:file:" + DB_DIR + "/keycloak.db;DB_CLOSE_ON_EXIT=true;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=0"
);
}
}

View file

@ -1,65 +1,240 @@
package org.keycloak.testframework.server;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.nio.file.StandardCopyOption;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.keycloak.it.utils.OutputConsumer;
import org.keycloak.it.utils.RawKeycloakDistribution;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.common.Version;
import org.keycloak.it.utils.Maven;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.representations.info.ServerInfoRepresentation;
import org.keycloak.testframework.util.FileUtils;
import org.keycloak.testframework.util.ProcessUtils;
import org.keycloak.testframework.util.TmpDir;
import io.quarkus.fs.util.ZipUtils;
import io.quarkus.maven.dependency.Dependency;
import org.jboss.logging.Logger;
import org.jetbrains.annotations.NotNull;
public class DistributionKeycloakServer implements KeycloakServer {
private static final boolean MANUAL_STOP = true;
private static final boolean RE_CREATE = false;
private static final boolean REMOVE_BUILD_OPTIONS_AFTER_BUILD = false;
private static final int REQUEST_PORT = 8080;
private static final Logger log = Logger.getLogger(DistributionKeycloakServer.class);
private RawKeycloakDistribution keycloak;
private static final File INSTALL_DIR = Path.of(TmpDir.resolveTmpDir().getAbsolutePath(), "kc-test-framework", "keycloak").toFile();
private static final String CMD = "kc" + (Environment.isWindows() ? ".bat" : ".sh");
private File keycloakHomeDir;
private Process keycloakProcess;
private final boolean debug;
private final boolean reuse;
private final long startTimeout;
private boolean tlsEnabled = false;
public DistributionKeycloakServer(boolean debug, long startTimeout) {
public DistributionKeycloakServer(boolean debug, boolean reuse, long startTimeout) {
this.debug = debug;
this.reuse = reuse;
this.startTimeout = startTimeout;
}
@Override
public void start(KeycloakServerConfigBuilder keycloakServerConfigBuilder, boolean tlsEnabled) {
this.tlsEnabled = tlsEnabled;
keycloak = new RawKeycloakDistribution(false, MANUAL_STOP, false, RE_CREATE, REMOVE_BUILD_OPTIONS_AFTER_BUILD, REQUEST_PORT, new LoggingOutputConsumer())
.withThreadDump(false)
.withThrowErrorIfFailedToStart(true);
if (startTimeout > 0) {
keycloak.withStartTimeout(startTimeout);
List<String> args = keycloakServerConfigBuilder.toArgs();
Set<Dependency> dependencies = keycloakServerConfigBuilder.toDependencies();
if (!reuse) {
killPreviousProcess();
}
// RawKeycloakDistribution sets "DEBUG_SUSPEND", not "DEBUG" when debug is passed to constructor
try {
boolean installationCreated = createInstallation();
File providersDir = new File(keycloakHomeDir, "providers");
List<File> existingProviders = listExistingProviders(providersDir);
if (!installationCreated && reuse && ping()) {
checkRunning();
File startupArgsFile = getServerArgsFile();
String startedWithArgs = startupArgsFile.isFile() ? FileUtils.readStringFromFile(startupArgsFile) : null;
String requestedArgs = String.join(" ", args);
Set<String> requestedDependencies = dependencies.stream().map(d -> d.getGroupId() + "__" + d.getArtifactId() + ".jar").collect(Collectors.toSet());
Set<String> startedWithDependencies = existingProviders.stream().map(File::getName).collect(Collectors.toSet());
if (requestedArgs.equals(startedWithArgs) && setEquals(requestedDependencies, startedWithDependencies)) {
log.trace("Re-using already running Keycloak");
return;
} else {
if (killPreviousProcess()) {
log.trace("Killed existing Keycloak");
} else {
throw new RuntimeException("Running Keycloak not started with required arguments or providers, and could not kill the current process");
}
}
}
updateProviders(existingProviders, dependencies, providersDir);
OutputHandler outputHandler = startKeycloak(args);
waitForStart(outputHandler);
FileUtils.writeToFile(getServerArgsFile(), String.join(" ", args));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void checkRunning() {
if (!Environment.isWindows()) {
ProcessBuilder pb = new ProcessBuilder("fuser", "-n", "tcp", tlsEnabled ? "8443" : "8080");
try {
Process process = pb.start();
process.waitFor(1, TimeUnit.SECONDS);
String pid = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim();
String expectedPid = FileUtils.readStringFromFile(getPidFile());
if (!pid.equals(expectedPid)) {
throw new RuntimeException("Process running on port is not a managed Keycloak server");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
ServerInfoRepresentation serverInfo;
try {
serverInfo = getServerInfo();
} catch (Throwable t) {
throw new RuntimeException("Non-managed Keycloak server or other process running on " + getBaseUrl());
}
File userDir = new File(serverInfo.getSystemInfo().getUserDir()).getParentFile();
if (!userDir.equals(keycloakHomeDir)) {
throw new RuntimeException("Non-managed Keycloak server running from " + userDir);
}
}
}
private @NotNull DistributionKeycloakServer.OutputHandler startKeycloak(List<String> args) {
log.trace("Starting Keycloak");
List<String> cmd = new LinkedList<>();
if (Environment.isWindows()) {
cmd.add(keycloakHomeDir.toPath().resolve("bin").resolve(CMD).toString());
} else {
cmd.add("./" + CMD);
}
cmd.addAll(args);
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.directory(new File(keycloakHomeDir, "bin"));
if (debug) {
keycloak.setEnvVar("DEBUG", "true");
pb.environment().put("DEBUG", "true");
}
for (Dependency dependency : keycloakServerConfigBuilder.toDependencies()) {
keycloak.copyProvider(dependency.getGroupId(), dependency.getArtifactId());
OutputHandler outputHandler;
try {
keycloakProcess = pb.start();
outputHandler = new OutputHandler(keycloakProcess);
new Thread(outputHandler).start();
ProcessHandle descendent = ProcessUtils.waitForDescendent(keycloakProcess);
FileUtils.writeToFile(getPidFile(), descendent.pid());
} catch (IOException e) {
throw new RuntimeException(e);
}
for (Path configFile : keycloakServerConfigBuilder.toConfigFiles()) {
keycloak.copyConfigFile(configFile);
}
return outputHandler;
}
keycloak.run(keycloakServerConfigBuilder.toArgs());
private static void updateProviders(List<File> existingProviders, Set<Dependency> dependencies, File providersDir) throws IOException {
existingProviders.stream()
.filter(f -> f.getName().endsWith(".jar"))
.filter(f -> {
String fileName = f.getName();
String groupId = fileName.substring(0, fileName.indexOf("__"));
String artifactId = fileName.substring(fileName.indexOf("__") + 2, fileName.lastIndexOf(".jar"));
return dependencies.stream().noneMatch(d -> d.getGroupId().equals(groupId) && d.getArtifactId().equals(artifactId));
}).forEach(f -> {
log.trace("Deleted non-requested provider: " + f.getAbsolutePath());
FileUtils.delete(f);
FileUtils.delete(new File(f.getAbsolutePath() + ".lastModified"));
});
Path providersPath = providersDir.toPath();
for (Dependency d : dependencies) {
Path dependencyPath = Maven.resolveArtifact(d.getGroupId(), d.getArtifactId());
File dependencyFile = dependencyPath.toFile();
Path targetPath = providersPath.resolve(d.getGroupId() + "__" + d.getArtifactId() + ".jar");
File targetFile = targetPath.toFile();
File targetLastModified = new File(targetFile.getAbsolutePath() + ".lastModified");
long lastModified = targetLastModified.isFile() ? FileUtils.readLongFromFile(targetLastModified) : -1;
if (lastModified != dependencyPath.toFile().lastModified() || !targetFile.isFile()) {
log.trace("Adding or overriding existing provider: " + targetPath.toFile().getAbsolutePath());
Files.copy(dependencyPath, targetPath, StandardCopyOption.REPLACE_EXISTING);
Files.writeString(targetLastModified.toPath(), Long.toString(dependencyFile.lastModified()));
}
}
}
@Override
public void stop() {
keycloak.stop();
if (!reuse) {
ProcessUtils.killRunningProcess(keycloakProcess);
File pidFile = getPidFile();
if (pidFile.exists()) {
FileUtils.delete(pidFile);
}
}
}
private boolean killPreviousProcess() {
if (!Environment.isWindows()) {
File pidFile = getPidFile();
if (pidFile.exists()) {
try {
String previousPid = FileUtils.readStringFromFile(pidFile);
if (ProcessUtils.killProcess(previousPid)) {
log.trace("Killed running managed Keycloak: " + previousPid);
FileUtils.delete(pidFile);
return true;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
return false;
}
@Override
@ -80,51 +255,204 @@ public class DistributionKeycloakServer implements KeycloakServer {
}
}
private static final class LoggingOutputConsumer implements OutputConsumer {
private boolean createInstallation() throws IOException {
File dist = resolveKeycloakDist();
if (INSTALL_DIR.isDirectory()) {
File[] f = INSTALL_DIR.listFiles();
if (f != null && f.length == 1) {
long fromZipLastModified = FileUtils.readLongFromFile(getZipLastModifiedFile(f[0]));
if (fromZipLastModified != dist.lastModified()) {
log.trace("Deleting installation from a previous distribution");
FileUtils.delete(INSTALL_DIR);
} else {
log.trace("Re-using previous installation");
keycloakHomeDir = f[0];
return false;
}
}
}
if (INSTALL_DIR.isDirectory()) {
FileUtils.delete(INSTALL_DIR);
}
if (!INSTALL_DIR.mkdirs()) {
throw new IOException("Failed to create directory " + INSTALL_DIR);
}
ZipUtils.unzip(dist.toPath(), INSTALL_DIR.toPath());
File[] files = INSTALL_DIR.listFiles();
if (files == null || files.length != 1) {
throw new RuntimeException("Expected " + INSTALL_DIR.getAbsolutePath() + " to contain a single directory");
}
keycloakHomeDir = files[0];
if (!Path.of(keycloakHomeDir.getPath(), "bin", CMD).toFile().setExecutable(true)) {
throw new RuntimeException("Failed to make startup script executable");
}
FileUtils.writeToFile(getZipLastModifiedFile(keycloakHomeDir), dist.lastModified());
return true;
}
private boolean ping() {
try {
HttpURLConnection urlConnection = (HttpURLConnection) new URL(getBaseUrl()).openConnection();
urlConnection.setConnectTimeout(1000);
urlConnection.setReadTimeout(1000);
if(urlConnection instanceof HttpsURLConnection httpsURLConnection) {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { new NullTrustManager() }, new SecureRandom());
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
httpsURLConnection.setSSLSocketFactory(socketFactory);
}
urlConnection.connect();
return true;
} catch (Exception e) {
return false;
}
}
private void waitForStart(OutputHandler outputHandler) {
boolean started = outputHandler.waitForStarted();
if (started && ping()) {
return;
}
keycloakProcess.destroy();
throw new RuntimeException("Keycloak did not start within timeout: " + getErrorOutput());
}
private File getZipLastModifiedFile(File dir) {
return new File(dir, "zip-last-modified");
}
private File getPidFile() {
return new File(keycloakHomeDir, "pid");
}
private File getServerArgsFile() {
return new File(keycloakHomeDir, "startup-args");
}
private ServerInfoRepresentation getServerInfo() {
KeycloakBuilder kcb = KeycloakBuilder.builder()
.serverUrl(getBaseUrl())
.realm("master")
.clientId("temp-admin")
.clientSecret("mysecret")
.grantType("client_credentials");
Keycloak kc = kcb.build();
ServerInfoRepresentation info = kc.serverInfo().getInfo();
kc.close();
return info;
}
private String getErrorOutput() {
try {
return new String(keycloakProcess.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
return "";
}
}
private static File resolveKeycloakDist() {
Path p = Path.of(System.getProperty("user.dir"));
String dist = "quarkus/dist/target/" + "keycloak-" + Version.VERSION + ".zip";
while (p.resolve("pom.xml").toFile().isFile()) {
File zip = p.resolve(dist).toFile();
if (zip.isFile()) {
return zip;
}
p = p.getParent();
}
return Maven.resolveArtifact("org.keycloak", "keycloak-quarkus-dist").toFile();
}
private boolean setEquals(Set<String> a, Set<String> b) {
return a.size() == b.size() && a.containsAll(b);
}
private List<File> listExistingProviders(File providersDir) {
if (providersDir.isDirectory()) {
File[] files = providersDir.listFiles(n -> n.getName().endsWith(".jar"));
if (files != null) {
return Arrays.stream(files).toList();
}
}
return List.of();
}
private class OutputHandler implements Runnable {
private static final Pattern LOG_PATTERN = Pattern.compile("([^ ]*) ([^ ]*) ([A-Z]*)([ ]*)(.*)");
private static final Logger LOGGER = Logger.getLogger("managed.keycloak");
@Override
public void onStdOut(String line) {
onLine(line, Logger.Level.INFO);
private boolean startedInPrinted = false;
private final Process process;
private CountDownLatch startupLatch = new CountDownLatch(1);
private OutputHandler(Process process) {
this.process = process;
}
@Override
public void onErrOut(String line) {
onLine(line, Logger.Level.ERROR);
}
public void run() {
InputStream is = process.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
try {
for (String line = br.readLine(); process.isAlive() && line != null; line = br.readLine()) {
if (!startedInPrinted && line.matches(".*Keycloak.* started in.*")) {
startupLatch.countDown();
}
@Override
public List<String> getStdOut() {
return Collections.emptyList();
}
@Override
public List<String> getErrOut() {
return Collections.emptyList();
}
private void onLine(String line, Logger.Level defaultLevel) {
Matcher matcher = LOG_PATTERN.matcher(line);
if (matcher.matches()) {
String levelString = matcher.group(3);
String message = matcher.group(5);
if (levelString != null && message != null) {
for (Logger.Level l : Logger.Level.values()) {
if (l.name().equals(levelString)) {
LOGGER.log(l, message);
return;
Matcher matcher = LOG_PATTERN.matcher(line);
if (matcher.matches()) {
String levelString = matcher.group(3);
String message = matcher.group(5);
if (levelString != null && message != null) {
for (Logger.Level l : Logger.Level.values()) {
if (l.name().equals(levelString)) {
LOGGER.log(l, message);
break;
}
}
}
}
LOGGER.info(line);
}
} catch (IOException e) {
// Ignored
}
}
LOGGER.log(defaultLevel, line);
public boolean waitForStarted() {
try {
startupLatch.await(startTimeout, TimeUnit.SECONDS);
return true;
} catch (InterruptedException e) {
return false;
}
}
}
private static class NullTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
@Override
public void reset() {
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}

View file

@ -7,15 +7,18 @@ public class DistributionKeycloakServerSupplier extends AbstractKeycloakServerSu
private static final Logger LOGGER = Logger.getLogger(DistributionKeycloakServerSupplier.class);
@ConfigProperty(name = "start.timeout", defaultValue = "-1")
@ConfigProperty(name = "start.timeout", defaultValue = "120")
long startTimeout;
@ConfigProperty(name = "debug", defaultValue = "false")
boolean debug;
boolean debug = false;
@ConfigProperty(name = "reuse", defaultValue = "false")
boolean reuse;
@Override
public KeycloakServer getServer() {
return new DistributionKeycloakServer(debug, startTimeout);
return new DistributionKeycloakServer(debug, reuse, startTimeout);
}
@Override

View file

@ -1,21 +1,15 @@
package org.keycloak.testframework.server;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import org.keycloak.Keycloak;
import org.keycloak.common.Version;
import org.keycloak.platform.Platform;
import io.quarkus.maven.dependency.Dependency;
public class EmbeddedKeycloakServer implements KeycloakServer {
private Keycloak keycloak;
private Path homeDir;
private boolean tlsEnabled = false;
@Override
@ -27,29 +21,6 @@ public class EmbeddedKeycloakServer implements KeycloakServer {
builder.addDependency(dependency.getGroupId(), dependency.getArtifactId(), "");
}
Set<Path> configFiles = keycloakServerConfigBuilder.toConfigFiles();
if (!configFiles.isEmpty()) {
if (homeDir == null) {
homeDir = Platform.getPlatform().getTmpDirectory().toPath();
}
Path conf = homeDir.resolve("conf");
if (!conf.toFile().exists()) {
conf.toFile().mkdirs();
}
for (Path configFile : configFiles) {
try {
Files.copy(configFile, conf.resolve(configFile.getFileName()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
builder.setHomeDir(homeDir);
keycloak = builder.start(keycloakServerConfigBuilder.toArgs());
}

View file

@ -1,15 +1,11 @@
package org.keycloak.testframework.server;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@ -31,7 +27,6 @@ public class KeycloakServerConfigBuilder {
private final Set<String> featuresDisabled = new HashSet<>();
private final LogBuilder log = new LogBuilder();
private final Set<Dependency> dependencies = new HashSet<>();
private final Set<Path> configFiles = new HashSet<>();
private CacheType cacheType = CacheType.LOCAL;
private boolean externalInfinispan = false;
@ -106,18 +101,6 @@ public class KeycloakServerConfigBuilder {
dependencies.add(new DependencyBuilder().setGroupId(groupId).setArtifactId(artifactId).build());
return this;
}
public KeycloakServerConfigBuilder cacheConfigFile(String resourcePath) {
try {
Path p = Paths.get(Objects.requireNonNull(getClass().getResource(resourcePath)).toURI());
configFiles.add(p);
option("cache-config-file", p.getFileName().toString());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
return this;
}
public class LogBuilder {
@ -235,10 +218,6 @@ public class KeycloakServerConfigBuilder {
return dependencies;
}
Set<Path> toConfigFiles() {
return configFiles;
}
private Set<String> toFeatureStrings(Profile.Feature... features) {
return Arrays.stream(features).map(f -> {
if (f.getVersion() > 1 || Profile.getFeatureVersions(f.getKey()).size() > 1) {

View file

@ -2,7 +2,6 @@ package org.keycloak.testframework.server;
import java.net.ConnectException;
import java.net.URL;
import java.nio.file.Path;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -71,15 +70,6 @@ public class RemoteKeycloakServer implements KeycloakServer {
}
out.println();
}
Set<Path> configFiles = config.toConfigFiles();
if (!configFiles.isEmpty()) {
out.println("Config files:");
for (Path c : configFiles) {
out.print("* " + c.toAbsolutePath());
}
out.println();
}
}
private void printStartupInstructionsKcw(KeycloakServerConfigBuilder config) {
@ -92,12 +82,6 @@ public class RemoteKeycloakServer implements KeycloakServer {
out.println("KCW_PROVIDERS=" + dependencyPaths + " \\");
}
Set<Path> configFiles = config.toConfigFiles();
if (!configFiles.isEmpty()) {
String configPaths = configFiles.stream().map(p -> p.toAbsolutePath().toString()).collect(Collectors.joining(","));
out.println("KCW_CONFIGS=" + configPaths + " \\");
}
out.println("kcw " + kcwCommand + " " + String.join(" \\\n", config.toArgs()));
out.println();
}

View file

@ -0,0 +1,48 @@
package org.keycloak.testframework.util;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class FileUtils {
public static long readLongFromFile(File file) {
return Long.parseLong(readStringFromFile(file));
}
public static String readStringFromFile(File file) {
try {
return org.apache.commons.io.FileUtils.readFileToString(file, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void writeToFile(File file, long value) {
writeToFile(file, Long.toString(value));
}
public static void writeToFile(File file, String value) {
try {
org.apache.commons.io.FileUtils.writeStringToFile(file, value);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void delete(File file) {
if (file.isFile()) {
if (!file.delete()) {
throw new RuntimeException("Failed to delete file: " + file.getAbsolutePath());
}
} else if (file.isDirectory()) {
try {
org.apache.commons.io.FileUtils.deleteDirectory(file);
} catch (IOException e) {
throw new RuntimeException("Failed to delete directory: " + e);
}
}
}
}

View file

@ -0,0 +1,78 @@
package org.keycloak.testframework.util;
import java.util.Iterator;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.keycloak.quarkus.runtime.Environment;
public class ProcessUtils {
public static ProcessHandle waitForDescendent(Process process) {
long timeout = System.currentTimeMillis() + 5000;
while (System.currentTimeMillis() < timeout) {
Optional<ProcessHandle> descendent = process.descendants().findFirst();
if (descendent.isPresent()) {
return descendent.get();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
throw new RuntimeException("Descendent process not started within timeout");
}
public static boolean killProcess(String pid) {
try {
if (!Environment.isWindows()) {
ProcessBuilder pb = new ProcessBuilder("kill", "--timeout", "10000", "TERM", "--timeout", "10000", "KILL", pid);
Process process = pb.start();
process.waitFor(10, TimeUnit.SECONDS);
return process.exitValue() == 0;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return false;
}
public static void killRunningProcess(Process process) {
killRunningProcess(process, false);
}
public static void killRunningProcess(Process process, boolean force) {
try {
if (Environment.isWindows()) {
CompletableFuture<?> allProcesses = CompletableFuture.completedFuture(null);
Iterator<ProcessHandle> itr = process.descendants().iterator();
while (itr.hasNext()) {
ProcessHandle ph = itr.next();
if (force) {
ph.destroyForcibly();
} else {
ph.destroy();
}
allProcesses = CompletableFuture.allOf(allProcesses, ph.onExit());
}
allProcesses.get(10, TimeUnit.SECONDS);
}
if (force) {
process.destroyForcibly();
} else {
process.destroy();
}
process.waitFor(10, TimeUnit.SECONDS);
} catch (Exception e) {
if (!force) {
killRunningProcess(process, true);
} else {
throw new RuntimeException("Failed to stop Keycloak process");
}
}
}
}

View file

@ -0,0 +1,20 @@
package org.keycloak.testframework.util;
import java.io.File;
public class TmpDir {
// Maven overrides java.io.tmpdir, resolving to OS tmp directory
public static File resolveTmpDir() {
File tmpDir = new File("/tmp");
if (tmpDir.isDirectory()) {
return tmpDir;
}
tmpDir = new File(System.getenv("TEMP"));
if (tmpDir.isDirectory()) {
return tmpDir;
}
return new File(System.getProperty("java.io.tmpdir"));
}
}

View file

@ -29,7 +29,7 @@ public class InfinispanXMLBackwardCompatibilityTest {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.cacheConfigFile(CONFIG_FILE);
return config.option("cache-config-file", getClass().getResource(CONFIG_FILE).getFile());
}
}
}