/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2026 3A Systems LLC. */ package org.openidentityplatform.opendj.embedded; import org.apache.commons.io.FileUtils; import org.testng.annotations.AfterMethod; import org.testng.annotations.Test; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; /** * Tests the deletion of the temporary directory of an embedded instance, which has to survive * the files that the server threads are still holding when {@code stop()} returns. *

* The deletion itself is injected, so that the retries can be driven without depending on a * genuinely undeletable file, which no platform offers in a portable way. */ public class EmbeddedOpenDJCleanupTest { private final List temporaryDirectories = new ArrayList<>(); @AfterMethod public void deleteTemporaryDirectories() { for (File directory : temporaryDirectories) { FileUtils.deleteQuietly(directory); } temporaryDirectories.clear(); } @Test public void deletesTheWholeTree() throws Exception { final File directory = createTree(3); EmbeddedOpenDJ.deleteInstanceDirectory(directory, 0); assertFalse(directory.exists(), directory + " has not been deleted"); } @Test public void ignoresADirectoryThatDoesNotExist() throws Exception { final File directory = createTree(0); assertTrue(FileUtils.deleteQuietly(directory)); //neither of these must throw EmbeddedOpenDJ.deleteInstanceDirectory(directory, 10_000); EmbeddedOpenDJ.deleteInstanceDirectory(null, 10_000); } @Test public void stopsRetryingAsSoonAsTheDeletionSucceeds() throws Exception { final File directory = createTree(1); final AtomicInteger attempts = new AtomicInteger(); final long elapsedMs = timed(() -> EmbeddedOpenDJ.deleteInstanceDirectory(directory, 10_000, file -> attempts.incrementAndGet() >= 3)); assertEquals(attempts.get(), 3, "the deletion should have been retried until it succeeded"); assertTrue(elapsedMs < 10_000, "it waited " + elapsedMs + " ms instead of returning on success"); } @Test public void triesOnlyOnceWithoutATimeout() throws Exception { final File directory = createTree(1); final AtomicInteger attempts = new AtomicInteger(); EmbeddedOpenDJ.deleteInstanceDirectory(directory, 0, file -> { attempts.incrementAndGet(); return false; }); assertEquals(attempts.get(), 1, "a zero timeout must not retry"); assertTrue(directory.exists(), "the injected deletion did not delete anything"); } @Test public void givesUpWhenTheTimeoutExpires() throws Exception { final File directory = createTree(1); final AtomicInteger attempts = new AtomicInteger(); final long timeoutMs = 300; final long elapsedMs = timed(() -> EmbeddedOpenDJ.deleteInstanceDirectory(directory, timeoutMs, file -> { attempts.incrementAndGet(); return false; })); assertTrue(attempts.get() > 1, "the deletion should have been retried, attempts: " + attempts.get()); assertTrue(elapsedMs >= timeoutMs, "it gave up after " + elapsedMs + " ms, before the timeout"); assertTrue(elapsedMs < TimeUnit.SECONDS.toMillis(30), "it did not give up, " + elapsedMs + " ms elapsed"); //the leftovers are registered for deletion on JVM exit, so this must return normally assertTrue(directory.exists()); } @Test public void givesUpWhenTheThreadIsInterrupted() throws Exception { final File directory = createTree(1); final AtomicInteger attempts = new AtomicInteger(); final long elapsedMs; Thread.currentThread().interrupt(); try { elapsedMs = timed(() -> EmbeddedOpenDJ.deleteInstanceDirectory(directory, 60_000, file -> { attempts.incrementAndGet(); return false; })); assertTrue(Thread.currentThread().isInterrupted(), "the interrupted status has been swallowed"); } finally { Thread.interrupted(); } assertEquals(attempts.get(), 1, "an interrupted deletion must not be retried"); assertTrue(elapsedMs < TimeUnit.SECONDS.toMillis(30), "it kept retrying for " + elapsedMs + " ms"); } @Test public void reportsTheRemainingPathsAndHowManyAreLeftOut() throws Exception { final File directory = createTree(25); final String remaining = EmbeddedOpenDJ.remainingPaths(directory); final String[] lines = remaining.split("\n"); //one empty leading token before the first line separator, 20 paths and the summary assertEquals(lines.length, 22, "unexpected report: " + remaining); for (int i = 1; i < lines.length - 1; i++) { assertTrue(lines[i].startsWith(" " + directory.getPath()), "unexpected line: " + lines[i]); } assertTrue(remaining.endsWith("... and 5 more"), remaining); } @Test public void reportsAllTheRemainingPathsWhenThereAreFewOfThem() throws Exception { final File directory = createTree(2); final String remaining = EmbeddedOpenDJ.remainingPaths(directory); assertEquals(remaining.split("\n").length, 3, "unexpected report: " + remaining); assertFalse(remaining.contains("more"), remaining); } /** Creates a temporary directory holding the given number of files, one of them nested. */ private File createTree(int fileCount) throws IOException { final File directory = Files.createTempDirectory("opendj-cleanup-test").toFile(); temporaryDirectories.add(directory); final File subDirectory = new File(directory, "nested"); assertTrue(subDirectory.mkdir()); for (int i = 0; i < fileCount; i++) { final File parent = i == 0 ? subDirectory : directory; Files.write(new File(parent, "file" + i + ".txt").toPath(), new byte[] { 'x' }); } return directory; } private static long timed(Runnable runnable) { final long start = System.nanoTime(); runnable.run(); return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); } }