mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
6 hours ago 0138b5924ff14ef709398bf878dbb9bf2a0b7c33
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
/*
 * 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;
 
import static java.nio.charset.Charset.defaultCharset;
import static java.nio.file.StandardOpenOption.APPEND;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
 
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
 
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.loggers.ErrorLogger;
import org.testng.SkipException;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
 
/**
 * Tests that a {@link TempLogFile} can be placed in a directory of the caller's choosing (the
 * instance {@code logs/} directory for setup, so that {@code start-ds} does not sweep it away
 * with the rest of {@code tmp/}, see issue #1030), and that it tells whether the file is still
 * there to be read.
 */
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "quicksetup" }, sequential = true)
public class TempLogFileTest extends DirectoryServerTestCase
{
  private static final String PREFIX = "opendj-setup-";
 
  private File tempDir;
  private final List<TempLogFile> created = new ArrayList<>();
 
  @BeforeClass
  public void setUp() throws IOException
  {
    tempDir = TestCaseUtils.createTemporaryDirectory("tempLogFileTest");
  }
 
  @AfterClass
  public void tearDown() throws IOException
  {
    for (TempLogFile logFile : created)
    {
      logFile.deleteLogFileAfterSuccess();
    }
    TestCaseUtils.deleteDirectory(tempDir);
  }
 
  private TempLogFile track(TempLogFile logFile)
  {
    created.add(logFile);
    return logFile;
  }
 
  private static File parentOf(TempLogFile logFile) throws IOException
  {
    return logFile.getLogFile().getCanonicalFile().getParentFile();
  }
 
  /** The directory does not exist before setup lays the instance down: it has to be created. */
  @Test
  public void testLogFileIsCreatedInTheRequestedDirectory() throws Exception
  {
    final File logs = new File(tempDir, "not-yet-laid-down/logs");
    assertFalse(logs.exists());
 
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, logs));
 
    assertTrue(logFile.isEnabled());
    assertTrue(logFile.isReadable());
    assertEquals(parentOf(logFile), logs.getCanonicalFile());
    assertTrue(logFile.getLogFile().getName().startsWith(PREFIX), logFile.getPath());
    assertTrue(logFile.getLogFile().getName().endsWith(".log"), logFile.getPath());
  }
 
  @Test
  public void testReadContentsReturnsWhatIsInTheFile() throws Exception
  {
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, new File(tempDir, "logs")));
    // The log's own stream is not in append mode and sits at the end of its own bytes, so a
    // record written after the marker would be written over it: shut the writer first, and
    // the marker is the last thing in the file whatever else the JVM logs.
    logFile.writer.shutdown();
    final String marker = "the last line written before the failure";
    Files.write(logFile.getLogFile().toPath(), (marker + "\n").getBytes(defaultCharset()), APPEND);
 
    assertTrue(logFile.readContents().endsWith(marker + "\n"));
  }
 
  /** Being enabled means messages are logged; being readable means the file is there to hand over. */
  @Test
  public void testIsReadableFollowsTheFileNotTheLogger() throws Exception
  {
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, new File(tempDir, "logs")));
    assertTrue(logFile.isReadable());
 
    // Not File.delete(): the writer still holds the file, and Windows does not delete a file
    // that is open. deleteLogFileAfterSuccess() shuts the writer first, as setup does.
    logFile.deleteLogFileAfterSuccess();
    assertFalse(logFile.getLogFile().exists());
 
    assertTrue(logFile.isEnabled());
    assertFalse(logFile.isReadable());
    try
    {
      logFile.readContents();
      fail("reading a deleted log must fail");
    }
    catch (IOException expected)
    {
      // the caller reports it instead of promising the file
    }
  }
 
  /** A directory where the log was is not a log: there is nothing to hand over either. */
  @Test
  public void testADirectoryAtTheLogPathIsNotReadable() throws Exception
  {
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, new File(tempDir, "logs")));
    logFile.deleteLogFileAfterSuccess();
    assertTrue(logFile.getLogFile().mkdir());
 
    assertTrue(Files.isReadable(logFile.getLogFile().toPath()));
    assertFalse(logFile.isReadable());
  }
 
  @Test
  public void testNoDirectoryMeansTheTemporaryDirectory() throws Exception
  {
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, null));
 
    assertTrue(logFile.isEnabled());
    assertEquals(parentOf(logFile), new File(System.getProperty("java.io.tmpdir")).getCanonicalFile());
  }
 
  /** A directory that cannot be used must not cost the log: fall back to the temporary directory. */
  @Test
  public void testUnusableDirectoryFallsBackToTheTemporaryDirectory() throws Exception
  {
    final File notADirectory = new File(tempDir, "not-a-directory");
    assertTrue(notADirectory.createNewFile());
 
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, notADirectory));
 
    assertTrue(logFile.isEnabled());
    assertTrue(logFile.isReadable());
    assertNotEquals(parentOf(logFile), notADirectory.getCanonicalFile());
    assertEquals(parentOf(logFile), new File(System.getProperty("java.io.tmpdir")).getCanonicalFile());
 
    // Why the log is not where it was asked for is warned about once there is a log to carry
    // the warning: at the point the directory failed, no publisher was installed yet.
    assumeTheLogGoesToTheFile();
    logFile.writer.shutdown();
    final String contents = logFile.readContents();
    assertTrue(contents.contains("falling back to the temporary directory"), contents);
    assertTrue(contents.contains(notADirectory.toString()), contents);
  }
 
  /**
   * The publishers the constructor puts on the logger singletons come off again with the log:
   * the singletons outlive the file, and what a leaked publisher is handed goes to a closed
   * stream and is swallowed.
   */
  @Test
  public void testDeletingTheLogTakesItsPublisherOffTheLogger() throws Exception
  {
    final int before = errorLogPublishers();
 
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, new File(tempDir, "logs")));
    assertEquals(errorLogPublishers(), before + 1, "the log logs through a publisher of its own");
 
    logFile.deleteLogFileAfterSuccess();
 
    assertEquals(errorLogPublishers(), before, "the publisher must not outlive the log");
  }
 
  /** How many publishers the error logger holds; {@code getLogPublishers()} is protected. */
  private static int errorLogPublishers() throws Exception
  {
    final Method getLogPublishers = ErrorLogger.class.getDeclaredMethod("getLogPublishers");
    getLogPublishers.setAccessible(true);
    return ((Collection<?>) getLogPublishers.invoke(ErrorLogger.getInstance())).size();
  }
 
  /**
   * {@code OPENDJ_LOG_TO_STDOUT} sends the records to stdout and leaves the file empty, so a
   * case which reads the log back has nothing to look at.
   */
  private static void assumeTheLogGoesToTheFile()
  {
    if ("true".equalsIgnoreCase(System.getenv("OPENDJ_LOG_TO_STDOUT")))
    {
      throw new SkipException("OPENDJ_LOG_TO_STDOUT writes the log to stdout, not to the file");
    }
  }
}