/* * 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.opends.quicksetup.installer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.opends.messages.QuickSetupMessages.*; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.forgerock.i18n.LocalizableMessage; import org.opends.quicksetup.TempLogFile; import org.opends.quicksetup.UserData; import org.opends.quicksetup.event.ProgressUpdateEvent; import org.opends.quicksetup.event.ProgressUpdateListener; import org.opends.quicksetup.util.PlainTextProgressMessageFormatter; import org.opends.server.DirectoryServerTestCase; import org.opends.server.TestCaseUtils; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; /** * Tests what a failed installation tells its listeners about the log file. *
* This report is the one place the diagnosis of a failed setup lives, and it used to promise
* the file without looking at it - printing a {@code NoSuchFileException} stack when the file
* was gone (issue #1030). The roads below are the whole of that decision.
*/
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "quicksetup" }, sequential = true)
public class InstallerTest extends DirectoryServerTestCase
{
private static final String PREFIX = "opendj-setup-";
/** Marks a line the report sends out as a warning, see {@link MarkedArmsFormatter}. */
private static final String WARNING = "[W]";
/** Marks a line the report sends out as progress. */
private static final String PROGRESS = "[P]";
private File tempDir;
private final List
* What the file holds is not the point here and is mocked away - with
* {@code OPENDJ_LOG_TO_STDOUT} set, a real log is empty and would pin nothing.
*/
@Test
public void testTheContentsOfTheLogReachTheReport() throws Exception
{
final String contents = "QuickSetup application launched\nthe last line before the failure\n";
final TempLogFile logFile = mock(TempLogFile.class);
when(logFile.isEnabled()).thenReturn(true);
when(logFile.isReadable()).thenReturn(true);
when(logFile.getPath()).thenReturn(new File(tempDir, "readable.log").getAbsolutePath());
when(logFile.readContents()).thenReturn(contents);
final String report = reportOf(logFile);
assertTrue(report.contains(contents), report);
}
/** A log something else removed is reported as gone, instead of being asked for. */
@Test
public void testAMissingLogIsReportedAsMissing() throws Exception
{
final TempLogFile logFile = newLogFile();
final String path = logFile.getPath();
logFile.deleteLogFileAfterSuccess();
assertFalse(logFile.isReadable());
final String report = reportOf(logFile);
assertContains(report, WARNING, INFO_GENERAL_LOG_IN_ERROR_MISSING.get(path));
// The line that asks for the file must not go out when there is no file to provide.
assertDoesNotContain(report, INFO_GENERAL_PROVIDE_LOG_IN_ERROR.get(path));
}
/** A log which is there but cannot be read costs the report its contents, not its diagnosis. */
@Test
public void testAnUnreadableLogIsReportedAsUnreadable() throws Exception
{
final String path = new File(tempDir, "unreadable.log").getAbsolutePath();
final IOException failure = new IOException("Input/output error");
final TempLogFile logFile = mock(TempLogFile.class);
when(logFile.isEnabled()).thenReturn(true);
when(logFile.isReadable()).thenReturn(true);
when(logFile.getPath()).thenReturn(path);
when(logFile.readContents()).thenThrow(failure);
final String report = reportOf(logFile);
assertContains(report, WARNING, INFO_GENERAL_LOG_IN_ERROR_UNREADABLE.get(path, failure));
assertDoesNotContain(report, INFO_GENERAL_LOG_IN_ERROR_MISSING.get(path));
}
/** Nothing is said about a log that was never created. */
@Test
public void testNoLogMeansNoReport() throws Exception
{
final TempLogFile logFile = mock(TempLogFile.class);
when(logFile.isEnabled()).thenReturn(false);
assertTrue(reportOf(logFile).isEmpty(), "a launcher without a log has nothing to report");
}
/**
* A cancelled install takes its log with it - and asks for it only when it begins.
*
* Nothing names the log on that road - the report belongs to the failure road - and the
* cancel has just taken the installation back, so a log kept there is a report nobody is
* ever pointed at (issue #1030). The installer is handed the log the way the wizard and the
* command line hand it over, as a supplier: nothing asks for it before {@code run()}.
*/
@Test
public void testACancelledInstallTakesItsLogWithIt() throws Exception
{
final TempLogFile logFile = newLogFile();
final File instance = new File(tempDir, "cancelled");
// A locks directory the lock file can be taken in: without it the cancel road reads the
// server as running and goes off to stop it.
assertTrue(new File(instance, "locks").mkdirs());
final TestInstaller installer = new TestInstaller(instance);
installer.setProgressMessageFormatter(new MarkedArmsFormatter());
final int[] asked = { 0 };
installer.setTempLogFile(() -> {
asked[0]++;
return logFile;
});
installer.setUserData(new UserData());
installer.cancel();
assertEquals(asked[0], 0, "nothing is logged before the install begins");
installer.run();
assertEquals(asked[0], 1, "the install asks for its log once, as it begins");
assertEquals(installer.getCurrentProgressStep(), InstallProgressStep.FINISHED_CANCELED);
assertFalse(logFile.getLogFile().exists(), logFile.getPath());
}
/**
* An application nobody handed a log to has none, and asking for it is not a failure: the
* command line uninstaller is built that way, its launcher having created the log already.
*/
@Test
public void testAnApplicationGivenNoLogHasNone() throws Exception
{
assertNull(new TestInstaller(null).openTempLogFile());
}
private TempLogFile newLogFile()
{
final TempLogFile logFile = TempLogFile.newTempLogFile(PREFIX, new File(tempDir, "logs"));
created.add(logFile);
return logFile;
}
/** What the listeners of a failed installation are told about the log file. */
private static String reportOf(final TempLogFile logFile)
{
final TestInstaller installer = new TestInstaller(null);
installer.setProgressMessageFormatter(new MarkedArmsFormatter());
final StringBuilder report = new StringBuilder();
installer.addProgressUpdateListener(new ProgressUpdateListener()
{
@Override
public void progressUpdate(final ProgressUpdateEvent ev)
{
if (ev.getNewLogs() != null)
{
report.append(ev.getNewLogs());
}
}
});
installer.setTempLogFile(() -> logFile);
// The report is made by an install which has begun, and so has asked for its log.
installer.openTempLogFile();
installer.notifyListenersOfExistingLogFile();
return report.toString();
}
private static void assertContains(final String report, final String arm, final LocalizableMessage expected)
{
assertTrue(report.contains(arm + expected), "expected <" + arm + expected + "> in <" + report + ">");
}
private static void assertDoesNotContain(final String report, final LocalizableMessage unexpected)
{
assertFalse(report.contains(unexpected.toString()), "unexpected <" + unexpected + "> in <" + report + ">");
}
/** An installer which can be told where it lives, and whose log can be asked for. */
private static final class TestInstaller extends Installer
{
/** The installation, or {@code null} for the one the class path names. */
private final File instance;
TestInstaller(final File instance)
{
this.instance = instance;
}
/**
* An installation under the temporary directory rather than the one the class path names:
* the installer takes both paths from there, and in a test run there is none.
*/
@Override
public String getInstallationPath()
{
return instance != null ? instance.getAbsolutePath() : super.getInstallationPath();
}
@Override
public String getInstancePath()
{
return instance != null ? instance.getAbsolutePath() : super.getInstancePath();
}
@Override
protected TempLogFile openTempLogFile()
{
return super.openTempLogFile();
}
}
/**
* A formatter that marks the arm each line goes out on.
*
* The plain text formatter returns warnings and progress messages unchanged - only the
* wizard's HTML formatter tells them apart - so without a mark a report which says "the log
* is gone" as an ordinary progress line reads exactly like one which warns about it.
*/
private static final class MarkedArmsFormatter extends PlainTextProgressMessageFormatter
{
@Override
public LocalizableMessage getFormattedWarning(final LocalizableMessage text, final boolean applyMargin)
{
return LocalizableMessage.raw(WARNING + super.getFormattedWarning(text, applyMargin));
}
@Override
public LocalizableMessage getFormattedProgress(final LocalizableMessage text)
{
return LocalizableMessage.raw(PROGRESS + super.getFormattedProgress(text));
}
}
}