/**
* Copyright 2013 the original author or authors.
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 io.neba.core.logviewer;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import static java.io.File.createTempFile;
import static java.nio.file.Files.move;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
/**
* @author Olaf Otto
*/
@RunWith(MockitoJUnitRunner.class)
public class TailTest extends TailTests {
private final ExecutorService executorService = newSingleThreadExecutor();
private Tail testee;
@After
public void tearDown() throws Exception {
if (this.testee != null) {
this.testee.stop();
}
this.executorService.shutdownNow();
}
@Test
public void testHandlingOfFileNotFoundExceptionDuringLogFileRotation() throws Exception {
File logFile = createTempFile("tailsocket-test-", ".log", this.getTestLogfileDirectory().getParentFile());
tailAsynchronously(logFile);
// Synchronously rotate the file when the first line is received
uponWriteToRemoteDo(() -> rotate(logFile));
write(logFile, "first line");
eventually(() -> assertSendTextContains("first line"));
// Wait before re-creating the rotated log file to make sure the file is temporarily not found by Tail
sleepUpTo(100, MILLISECONDS);
// At this point, the first line was read and the logfile was synchronously rotated immediately thereafter.
// Tail should have tolerated that the logfile was temporarily gone, and should now notice that the
// file was rotated as a new blank file is in the original file's place.
createFile(logFile.getAbsolutePath());
eventually(() -> assertErrorMessageIsSent("file rotated"));
}
@Test
public void testHandlingOfFileRotation() throws Exception {
File logFile = createTempFile("tailsocket-test-", ".log", this.getTestLogfileDirectory().getParentFile());
tailAsynchronously(logFile);
// When the first line is read, synchronously rotate and re-create the log file.
uponWriteToRemoteDo(() -> {
rotate(logFile);
createFile(logFile.getAbsolutePath());
return null;
});
write(logFile, "first line");
eventually(() -> assertSendTextContains("first line"));
eventually(() -> assertErrorMessageIsSent("file rotated"));
}
@Test
public void testHandlingOfRemovedLogFile() throws Exception {
File logFile = createTempFile("tailsocket-test-", ".log", this.getTestLogfileDirectory().getParentFile());
tailAsynchronously(logFile);
// Synchronously rotate the logfile when the first line is read. Do not re-created it.
uponWriteToRemoteDo(() -> rotate(logFile));
write(logFile, "first line");
eventually(() -> assertSendTextContains("first line"));
eventually(() -> assertErrorMessageIsSent("file not found"));
}
@Test
public void testHandlingOfIoExceptionWhenSendingLineToClient() throws Exception {
doThrow(new IOException("THIS IS AN EXPECTED TEST EXCEPTION"))
.when(this.getRemote())
.sendBytes(any());
tailSynchronously("logs/error.log");
}
@Test
public void testPreservationOfWhiteSpaces() throws Exception {
tailAsynchronously("logs/error-withwhitespaces.log");
eventually(() ->
assertSendTextContains(
"06.09.2013 15:03:50.719 *ERROR* error message with stacktrace\r\n" +
" at org.apache.sling.jcr.resource.internal.JcrResourceResolverFactoryImpl.getDefaultWorkspaceName(JcrResourceResolverFactoryImpl.java:398)\r\n" +
" at org.apache.sling.jcr.resource.internal.JcrResourceResolver.getResource(JcrResourceResolver.java:817)"));
}
@Test
public void testTailErrorLogIsFullyRead() throws Exception {
tailAsynchronously("logs/error.log");
eventually(() -> {
assertSendTextStartsWith("-- test logs/error.log first line --");
assertSendTextEndsWith("-- test logs/error.log last line --");
});
}
@Test(expected = IllegalArgumentException.class)
public void testHandlingOfNullFileArgument() throws Exception {
new Tail(mock(RemoteEndpoint.class), null, 1000);
}
@Test(expected = IllegalArgumentException.class)
public void testHandlingOfNullRemoteArgument() throws Exception {
new Tail(null, mock(File.class), 1000);
}
private void assertSendTextEndsWith(String s) {
assertThat(this.getReceivedText().toString()).endsWith(s);
}
private void assertSendTextStartsWith(String s) {
assertThat(this.getReceivedText().toString()).startsWith(s);
}
private void assertErrorMessageIsSent(String message) {
try {
verify(this.getRemote()).sendString(message);
} catch (IOException e) {
// Cannot happen, the remote is a mock.
}
}
private void tailSynchronously(String fileName) {
this.testee = new Tail(getRemote(), new File(getTestLogfileDirectory(), fileName), 1024L * 1024L);
this.testee.run();
}
private void tailAsynchronously(String fileName) {
this.testee = new Tail(getRemote(), new File(getTestLogfileDirectory(), fileName), 1024L * 1024L);
this.executorService.execute(this.testee);
}
private void tailAsynchronously(File logFile) {
this.testee = new Tail(getRemote(), logFile, 1024);
this.executorService.execute(this.testee);
}
private Path rotate(File file) throws IOException {
return move(file.toPath(), new File(file.getAbsolutePath() + ".rotated").toPath());
}
private void createFile(String logFilePath) throws IOException {
assertThat(new File(logFilePath).createNewFile())
.describedAs("Re-creating the logfile was successful")
.isTrue();
}
}