| // Copyright 2018 The Bazel Authors. All rights reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.devtools.build.lib.util; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static com.google.common.truth.Truth8.assertThat; | |
| import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import com.google.common.base.Strings; | |
| import com.google.devtools.build.lib.util.SimpleLogHandler.HandlerQuerier; | |
| import java.io.BufferedReader; | |
| import java.io.File; | |
| import java.io.FileInputStream; | |
| import java.io.FileOutputStream; | |
| import java.io.IOException; | |
| import java.io.InputStreamReader; | |
| import java.io.OutputStreamWriter; | |
| import java.nio.file.DirectoryStream; | |
| import java.nio.file.Files; | |
| import java.nio.file.Path; | |
| import java.nio.file.Paths; | |
| import java.text.SimpleDateFormat; | |
| import java.time.Clock; | |
| import java.time.Instant; | |
| import java.time.ZoneId; | |
| import java.time.ZoneOffset; | |
| import java.util.Arrays; | |
| import java.util.Date; | |
| import java.util.Optional; | |
| import java.util.TimeZone; | |
| import java.util.logging.FileHandler; | |
| import java.util.logging.Formatter; | |
| import java.util.logging.Level; | |
| import java.util.logging.LogRecord; | |
| import java.util.logging.Logger; | |
| import org.junit.Rule; | |
| import org.junit.Test; | |
| import org.junit.rules.TemporaryFolder; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| /** Tests for the {@link SimpleLogHandler} class. */ | |
| @RunWith(JUnit4.class) | |
| public final class SimpleLogHandlerTest { | |
| @Rule public TemporaryFolder tmp = new TemporaryFolder(); | |
| @Test | |
| public void testPrefix() throws Exception { | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "hello_world_%u%h%%_") | |
| .build(); | |
| handler.publish(new LogRecord(Level.SEVERE, "Hello world")); // To open the log file. | |
| assertThat(handler.getCurrentLogFilePath().get().toString()) | |
| .startsWith(tmp.getRoot() + File.separator + "hello_world_%u%h%%_"); | |
| } | |
| @Test | |
| public void testPatternVariables() throws Exception { | |
| String username = System.getProperty("user.name"); | |
| if (Strings.isNullOrEmpty(username)) { | |
| username = "unknown_user"; | |
| } | |
| String hostname = SimpleLogHandler.getLocalHostnameFirstComponent(); | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "hello_") | |
| .setPattern("world_%u%%%h_") | |
| .build(); | |
| handler.publish(new LogRecord(Level.SEVERE, "Hello world")); // To open the log file. | |
| assertThat(handler.getCurrentLogFilePath().get().toString()) | |
| .startsWith( | |
| tmp.getRoot() + File.separator + "hello_world_" + username + "%" + hostname + "_"); | |
| } | |
| @Test | |
| public void testPatternInvalidVariable() throws Exception { | |
| assertThrows( | |
| IllegalArgumentException.class, | |
| () -> SimpleLogHandler.builder().setPattern("hello_%t").build()); | |
| } | |
| @Test | |
| public void testExtensionDefaults() throws Exception { | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder().setPrefix(tmp.getRoot() + File.separator + "hello").build(); | |
| handler.publish(new LogRecord(Level.SEVERE, "Hello world")); // To open the log file. | |
| assertThat(handler.getCurrentLogFilePath().get().toString()) | |
| .endsWith("." + SimpleLogHandler.getPidString()); | |
| } | |
| @Test | |
| public void testExtensionSetter() throws Exception { | |
| SimpleLogHandler handler1 = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "hello") | |
| .setExtension("xyz") | |
| .build(); | |
| handler1.publish(new LogRecord(Level.SEVERE, "Hello world")); // To open the log file. | |
| assertThat(handler1.getCurrentLogFilePath().get().toString()).endsWith(".xyz"); | |
| } | |
| private static final class FakeClock extends Clock { | |
| private Instant now; | |
| private final ZoneId zone; | |
| FakeClock(Instant now, ZoneId zone) { | |
| this.now = now; | |
| this.zone = zone; | |
| } | |
| void set(Instant now) { | |
| this.now = now; | |
| } | |
| @Override | |
| public Instant instant() { | |
| return now; | |
| } | |
| @Override | |
| public ZoneId getZone() { | |
| return zone; | |
| } | |
| @Override | |
| public Clock withZone(ZoneId zone) { | |
| return new FakeClock(this.now, zone); | |
| } | |
| } | |
| @Test | |
| public void testTimestamp() throws Exception { | |
| Instant instant = Instant.parse("2015-09-01T15:17:54Z"); | |
| FakeClock clock = new FakeClock(instant, ZoneOffset.UTC); | |
| SimpleDateFormat dateFormat = new SimpleDateFormat(SimpleLogHandler.DEFAULT_TIMESTAMP_FORMAT); | |
| dateFormat.setTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC)); | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "hello") | |
| .setClockForTesting(clock) | |
| .build(); | |
| handler.publish(new LogRecord(Level.SEVERE, "Hello world")); // To open the log file. | |
| assertThat(dateFormat.format(Date.from(instant))).isEqualTo("20150901-151754."); | |
| assertThat(handler.getCurrentLogFilePath().get().toString()).contains("20150901-151754."); | |
| } | |
| private static final class TrivialFormatter extends Formatter { | |
| @Override | |
| public String format(LogRecord rec) { | |
| return formatMessage(rec) + "\n"; | |
| } | |
| } | |
| @Test | |
| public void testPublish() throws Exception { | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "hello") | |
| .setFormatter(new TrivialFormatter()) | |
| .build(); | |
| handler.publish(new LogRecord(Level.SEVERE, "Hello world")); // To open the log file. | |
| Path logPath = handler.getCurrentLogFilePath().get(); | |
| handler.close(); | |
| assertThat(new String(Files.readAllBytes(logPath), UTF_8)).isEqualTo("Hello world\n"); | |
| } | |
| @Test | |
| public void testSymbolicLinkDefaults() throws Exception { | |
| Path symlinkPath = Paths.get(tmp.getRoot().toString(), "hello"); | |
| Files.createFile(symlinkPath); | |
| // On non-Windows platforms, expect to delete the file at symlinkPath and replace with a symlink | |
| // to the log. | |
| SimpleLogHandler handler = SimpleLogHandler.builder().setPrefix(symlinkPath.toString()).build(); | |
| handler.publish(new LogRecord(Level.SEVERE, "Hello world")); // To open the log file. | |
| if (OS.getCurrent() == OS.WINDOWS) { | |
| // On Windows, by default, only administrator accounts can create symbolic links. | |
| assertThat(handler.getSymbolicLinkPath()).isEmpty(); | |
| } else { | |
| assertThat(handler.getSymbolicLinkPath()).isPresent(); | |
| assertThat(handler.getSymbolicLinkPath().get().toString()).isEqualTo(symlinkPath.toString()); | |
| assertThat(Files.isSymbolicLink(handler.getSymbolicLinkPath().get())).isTrue(); | |
| assertThat(Files.readSymbolicLink(handler.getSymbolicLinkPath().get()).toString()) | |
| .isEqualTo(handler.getCurrentLogFilePath().get().getFileName().toString()); | |
| } | |
| } | |
| @Test | |
| public void testSymbolicLinkSetter() throws Exception { | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "hello") | |
| .setSymlinkName("bye") | |
| .build(); | |
| handler.publish(new LogRecord(Level.SEVERE, "Hello world")); // To open the log file. | |
| if (OS.getCurrent() == OS.WINDOWS) { | |
| // On Windows, by default, only administrator accounts can create symbolic links. | |
| assertThat(handler.getSymbolicLinkPath()).isEmpty(); | |
| } else { | |
| assertThat(handler.getSymbolicLinkPath()).isPresent(); | |
| assertThat(handler.getSymbolicLinkPath().get().toString()) | |
| .isEqualTo(tmp.getRoot() + File.separator + "bye"); | |
| assertThat(Files.isSymbolicLink(handler.getSymbolicLinkPath().get())).isTrue(); | |
| assertThat(Files.readSymbolicLink(handler.getSymbolicLinkPath().get()).toString()) | |
| .isEqualTo(handler.getCurrentLogFilePath().get().getFileName().toString()); | |
| } | |
| } | |
| @Test | |
| public void testSymlinkEnabling() throws Exception { | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "hello") | |
| .setSymlinkName("bye") | |
| .setCreateSymlink(true) | |
| .build(); | |
| assertThat(handler.getSymbolicLinkPath()).isPresent(); | |
| } | |
| @Test | |
| public void testSymlinkDisabling() throws Exception { | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "hello") | |
| .setSymlinkName("bye") | |
| .setCreateSymlink(false) | |
| .build(); | |
| assertThat(handler.getSymbolicLinkPath()).isEmpty(); | |
| } | |
| @Test | |
| public void testSymbolicLinkInvalidPath() throws Exception { | |
| // "bye/bye" is invalid as a symlink path - it's not at the top level of log directory. | |
| SimpleLogHandler.Builder builder = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "hello") | |
| .setSymlinkName("bye" + File.separator + "bye") | |
| .setCreateSymlink(true); | |
| assertThrows(IllegalArgumentException.class, () -> builder.build()); | |
| } | |
| @Test | |
| public void testSymbolicLinkInitiallyInvalidReplaced() throws Exception { | |
| if (OS.getCurrent() == OS.WINDOWS) { | |
| // On Windows, by default, only administrator accounts can create symbolic links. | |
| return; | |
| } | |
| Path symlinkPath = Paths.get(tmp.getRoot().toString(), "hello"); | |
| Files.createSymbolicLink(symlinkPath, Paths.get("no-such-file")); | |
| // Expected to delete the (invalid) symlink and replace with a symlink to the log | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder().setPrefix(symlinkPath.toString()).build(); | |
| handler.publish(new LogRecord(Level.SEVERE, "Hello world")); // To open the log file. | |
| assertThat(handler.getSymbolicLinkPath().get().toString()).isEqualTo(symlinkPath.toString()); | |
| assertThat(Files.isSymbolicLink(handler.getSymbolicLinkPath().get())).isTrue(); | |
| assertThat(Files.readSymbolicLink(handler.getSymbolicLinkPath().get()).toString()) | |
| .isEqualTo(handler.getCurrentLogFilePath().get().getFileName().toString()); | |
| } | |
| @Test | |
| public void testLogLevelEqualPublished() throws Exception { | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "info") | |
| .setLogLevel(Level.INFO) | |
| .build(); | |
| handler.publish(new LogRecord(Level.INFO, "Hello")); | |
| Optional<Path> logPath = handler.getCurrentLogFilePath(); | |
| handler.close(); | |
| assertThat(Files.size(logPath.get())).isGreaterThan(0L); | |
| } | |
| @Test | |
| public void testLogLevelHigherPublished() throws Exception { | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "info") | |
| .setLogLevel(Level.INFO) | |
| .build(); | |
| handler.publish(new LogRecord(Level.WARNING, "Hello")); | |
| Optional<Path> logPath = handler.getCurrentLogFilePath(); | |
| handler.close(); | |
| assertThat(Files.size(logPath.get())).isGreaterThan(0L); | |
| } | |
| @Test | |
| public void testLogLevelLowerNotPublished() throws Exception { | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "info") | |
| .setLogLevel(Level.INFO) | |
| .build(); | |
| handler.publish(new LogRecord(Level.FINE, "Hello")); | |
| Optional<Path> logPath = handler.getCurrentLogFilePath(); | |
| handler.close(); | |
| assertThat(logPath.isPresent()).isFalse(); | |
| } | |
| @Test | |
| public void testLogLevelDefaultAllPublished() throws Exception { | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder().setPrefix(tmp.getRoot() + File.separator + "all").build(); | |
| handler.publish(new LogRecord(Level.FINEST, "Hello")); | |
| Optional<Path> logPath = handler.getCurrentLogFilePath(); | |
| handler.close(); | |
| assertThat(Files.size(logPath.get())).isGreaterThan(0L); | |
| } | |
| @Test | |
| public void testRotateLimitBytes() throws Exception { | |
| FakeClock clock = new FakeClock(Instant.parse("2018-01-01T12:00:00Z"), ZoneOffset.UTC); | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "limits") | |
| .setFormatter(new TrivialFormatter()) | |
| .setRotateLimitBytes(16) | |
| .setClockForTesting(clock) | |
| .build(); | |
| Optional<Path> symlinkPath = handler.getSymbolicLinkPath(); | |
| handler.publish(new LogRecord(Level.SEVERE, "1234567" /* 8 bytes including "\n" */)); | |
| Path firstLogPath = handler.getCurrentLogFilePath().get(); | |
| clock.set(Instant.parse("2018-01-01T12:00:01Z")); // Ensure the next file has a different name. | |
| handler.publish(new LogRecord(Level.SEVERE, "1234567" /* 8 bytes including "\n" */)); | |
| Path secondLogPath = handler.getCurrentLogFilePath().get(); | |
| handler.publish(new LogRecord(Level.SEVERE, "1234567" /* 8 bytes including "\n" */)); | |
| handler.close(); | |
| if (symlinkPath.isPresent()) { | |
| // The symlink path is expected to be present on non-Windows platforms; see tests above. | |
| assertThat(Files.isSymbolicLink(symlinkPath.get())).isTrue(); | |
| assertThat(Files.readSymbolicLink(symlinkPath.get()).toString()) | |
| .isEqualTo(secondLogPath.getFileName().toString()); | |
| } | |
| assertThat(Files.size(firstLogPath)).isEqualTo(16L /* including two "\n" */); | |
| assertThat(Files.size(secondLogPath)).isEqualTo(8L /* including "\n" */); | |
| try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(tmp.getRoot().toPath())) { | |
| assertThat(dirStream).hasSize(3); | |
| } | |
| } | |
| private Path newFileWithContent(String name, String content) throws IOException { | |
| File file = tmp.newFile(name); | |
| try (OutputStreamWriter writer = | |
| new OutputStreamWriter(new FileOutputStream(file.getPath()), UTF_8)) { | |
| writer.write(content); | |
| } | |
| return file.toPath(); | |
| } | |
| private Path newFileOfSize(String name, int size) throws IOException { | |
| char[] buf = new char[size]; | |
| Arrays.fill(buf, '\n'); | |
| return newFileWithContent(name, new String(buf)); | |
| } | |
| @Test | |
| public void testOpenInAppendMode() throws Exception { | |
| Path logPath = newFileWithContent("hello.20150901-151754.log", "Previous logs\n"); | |
| Instant instant = Instant.parse("2015-09-01T15:17:54Z"); | |
| FakeClock clock = new FakeClock(instant, ZoneOffset.UTC); | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "hello") | |
| .setPattern(".") | |
| .setExtension("log") | |
| .setFormatter(new TrivialFormatter()) | |
| .setClockForTesting(clock) | |
| .build(); | |
| handler.publish(new LogRecord(Level.SEVERE, "New logs")); | |
| assertThat(handler.getCurrentLogFilePath().get().toString()).isEqualTo(logPath.toString()); | |
| handler.close(); | |
| try (BufferedReader logReader = | |
| new BufferedReader(new InputStreamReader(new FileInputStream(logPath.toFile()), UTF_8))) { | |
| assertThat(logReader.readLine()).isEqualTo("Previous logs"); | |
| assertThat(logReader.readLine()).isEqualTo("New logs"); | |
| } | |
| } | |
| @Test | |
| public void testTotalLimit() throws Exception { | |
| String username = System.getProperty("user.name"); | |
| if (Strings.isNullOrEmpty(username)) { | |
| username = "unknown_user"; | |
| } | |
| String hostname = SimpleLogHandler.getLocalHostnameFirstComponent(); | |
| String baseFilename = "hello." + hostname + "." + username + ".log.java."; | |
| Path nonLog = newFileOfSize("non_log", 16); | |
| Path missingDate = newFileOfSize(baseFilename + ".123", 16); | |
| Path invalidExtension = newFileOfSize(baseFilename + "19900101-120000.invalid", 16); | |
| Path oldDeleted1 = newFileOfSize(baseFilename + "19900101-120000.123", 16); | |
| Path oldDeleted2 = newFileOfSize(baseFilename + "19950101-120000.123", 16); | |
| Path keptThenDeleted = newFileOfSize(baseFilename + "19990101-120000.123", 16); | |
| Path kept = newFileOfSize(baseFilename + "19990606-060000.123", 16); | |
| FakeClock clock = new FakeClock(Instant.parse("2018-01-01T12:00:00Z"), ZoneOffset.UTC); | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder() | |
| .setPrefix(tmp.getRoot() + File.separator + "hello") | |
| .setPattern(".%h.%u.log.java.") | |
| .setFormatter(new TrivialFormatter()) | |
| .setRotateLimitBytes(16) | |
| .setTotalLimitBytes(40) | |
| .setClockForTesting(clock) | |
| .build(); | |
| // Print 8 bytes into the log file. Opening the log file triggers deletion of old logs. | |
| handler.publish(new LogRecord(Level.SEVERE, "1234567" /* 8 bytes including "\n" */)); | |
| // We expect handler to delete all but 32 = 40 - 8 bytes worth of old log files. | |
| assertThat(Files.exists(nonLog)).isTrue(); | |
| assertThat(Files.exists(missingDate)).isTrue(); | |
| assertThat(Files.exists(invalidExtension)).isTrue(); | |
| assertThat(Files.exists(oldDeleted1)).isFalse(); | |
| assertThat(Files.exists(oldDeleted2)).isFalse(); | |
| assertThat(Files.exists(keptThenDeleted)).isTrue(); | |
| assertThat(Files.exists(kept)).isTrue(); | |
| handler.publish(new LogRecord(Level.SEVERE, "1234567" /* 8 bytes including "\n" */)); | |
| Path currentLogPath = handler.getCurrentLogFilePath().get(); | |
| handler.close(); | |
| // We expect another old log file to be deleted after rotation. | |
| assertThat(Files.exists(keptThenDeleted)).isFalse(); | |
| assertThat(Files.exists(kept)).isTrue(); | |
| assertThat(Files.exists(currentLogPath)).isTrue(); | |
| } | |
| @Test | |
| public void getLoggerFilePath_onSimpleLogHandler_withFile_returnsPath() throws Exception { | |
| HandlerQuerier handlerQuerier = new HandlerQuerier(); | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder().setPrefix(tmp.getRoot() + File.separator + "hello").build(); | |
| Logger logger = Logger.getAnonymousLogger(); | |
| logger.addHandler(handler); | |
| handler.publish(new LogRecord(Level.SEVERE, "Hello world")); // Ensure log file is opened. | |
| Optional<Path> retrievedLogPath = handlerQuerier.getLoggerFilePath(logger); | |
| assertThat(retrievedLogPath).isPresent(); | |
| assertThat(retrievedLogPath.get().toString()) | |
| .startsWith(tmp.getRoot() + File.separator + "hello"); | |
| handler.close(); | |
| } | |
| @Test | |
| public void getLoggerFilePath_onSimpleLogHandler_withoutFile_returnsEmpty() throws Exception { | |
| HandlerQuerier handlerQuerier = new HandlerQuerier(); | |
| SimpleLogHandler handler = | |
| SimpleLogHandler.builder().setPrefix(tmp.getRoot() + File.separator + "hello").build(); | |
| Logger logger = Logger.getAnonymousLogger(); | |
| logger.addHandler(handler); | |
| assertThat(handlerQuerier.getLoggerFilePath(logger)).isEmpty(); | |
| } | |
| @Test | |
| public void getLoggerFilePath_onUnsupportedLogHandler_fails() throws Exception { | |
| HandlerQuerier handlerQuerier = new HandlerQuerier(); | |
| FileHandler unsupportedHandler = new FileHandler(tmp.getRoot() + File.separator + "hello"); | |
| Logger logger = Logger.getAnonymousLogger(); | |
| logger.addHandler(unsupportedHandler); | |
| assertThrows(IllegalArgumentException.class, () -> handlerQuerier.getLoggerFilePath(logger)); | |
| unsupportedHandler.close(); | |
| } | |
| @Test | |
| public void getLoggerFilePath_onMissingLogHandler_fails() throws Exception { | |
| HandlerQuerier handlerQuerier = new HandlerQuerier(); | |
| Logger logger = Logger.getAnonymousLogger(); | |
| assertThrows(IllegalArgumentException.class, () -> handlerQuerier.getLoggerFilePath(logger)); | |
| } | |
| } |