/*-
* -\-\-
* Helios Testing Library
* --
* Copyright (C) 2016 Spotify AB
* --
* 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.spotify.helios.testing;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.emptyCollectionOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.spotify.docker.client.LogMessage;
import com.spotify.helios.common.descriptors.JobId;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Iterator;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@SuppressWarnings("AvoidEscapedUnicodeCharacters")
public class LoggingLogStreamFollowerTest {
private final LoggerContext context = new LoggerContext();
private final Logger log = context.getLogger("test");
private final CapturingAppender appender = CapturingAppender.create();
@Before
public void setUp() throws Exception {
context.start();
appender.start();
log.setLevel(Level.ALL);
log.addAppender(appender);
}
@After
public void tearDown() throws Exception {
appender.stop();
context.stop();
}
@Test
public void testSingleLine() throws Exception {
final Iterator<LogMessage> stream = stream(stdout("abc123\n"));
final LoggingLogStreamFollower sut = LoggingLogStreamFollower.create(log);
sut.followLog(JobId.fromString("a:b:c"), "d", stream);
assertThat(appender.events(),
contains(event(Level.INFO, "[a] [d] 1 abc123")));
}
@Test
public void testSingleLineNoNewline() throws Exception {
final Iterator<LogMessage> stream = stream(stdout("abc123"));
final LoggingLogStreamFollower sut = LoggingLogStreamFollower.create(log);
sut.followLog(JobId.fromString("a:b:c"), "d", stream);
assertThat(appender.events(),
contains(event(Level.INFO, "[a] [d] 1 abc123")));
}
@Test
public void testTwoLines() throws Exception {
final Iterator<LogMessage> stream = stream(stdout("abc123\n123abc\n"));
final LoggingLogStreamFollower sut = LoggingLogStreamFollower.create(log);
sut.followLog(JobId.fromString("a:b:c"), "d", stream);
assertThat(appender.events(),
contains(event(Level.INFO, "[a] [d] 1 abc123"),
event(Level.INFO, "[a] [d] 1 123abc")));
}
@Test
public void testTwoLinesNoNewline() throws Exception {
final Iterator<LogMessage> stream = stream(stdout("abc123\n123abc"));
final LoggingLogStreamFollower sut = LoggingLogStreamFollower.create(log);
sut.followLog(JobId.fromString("a:b:c"), "d", stream);
assertThat(appender.events(),
contains(event(Level.INFO, "[a] [d] 1 abc123"),
event(Level.INFO, "[a] [d] 1 123abc")));
}
@Test
public void testInterleavedStreams() throws Exception {
final Iterator<LogMessage> stream =
stream(stdout("abc"), stderr("123"), stdout("123"), stderr("abc"));
final LoggingLogStreamFollower sut = LoggingLogStreamFollower.create(log);
sut.followLog(JobId.fromString("a:b:c"), "d", stream);
assertThat(appender.events(),
contains(event(Level.INFO, "[a] [d] 1 abc"),
event(Level.INFO, "[a] [d] 2 123"),
event(Level.INFO, "[a] [d] 1 123"),
event(Level.INFO, "[a] [d] 2 abc")));
}
@Test
public void testInterleavedStreamsNewline() throws Exception {
final Iterator<LogMessage> stream =
stream(stdout("abc\n"), stderr("123\n"), stdout("123\n"), stderr("abc\n"));
final LoggingLogStreamFollower sut = LoggingLogStreamFollower.create(log);
sut.followLog(JobId.fromString("a:b:c"), "d", stream);
assertThat(appender.events(),
contains(event(Level.INFO, "[a] [d] 1 abc"),
event(Level.INFO, "[a] [d] 2 123"),
event(Level.INFO, "[a] [d] 1 123"),
event(Level.INFO, "[a] [d] 2 abc")));
}
@Test
public void testPartialUtf8() throws Exception {
final byte[] annoyingData = "foo\u0000\uffff௵\uD808\uDC30".getBytes(Charsets.UTF_8);
assertThat(annoyingData.length, is(14));
final Iterator<LogMessage> stream =
stream(stdout(Arrays.copyOfRange(annoyingData, 0, 1)),
stdout(Arrays.copyOfRange(annoyingData, 1, 2)),
stdout(Arrays.copyOfRange(annoyingData, 2, 3)),
stdout(Arrays.copyOfRange(annoyingData, 3, 4)),
stdout(Arrays.copyOfRange(annoyingData, 4, 5)),
stdout(Arrays.copyOfRange(annoyingData, 5, 6)),
stdout(Arrays.copyOfRange(annoyingData, 6, 7)),
stdout(Arrays.copyOfRange(annoyingData, 7, 8)),
stdout(Arrays.copyOfRange(annoyingData, 8, 9)),
stdout(Arrays.copyOfRange(annoyingData, 9, 10)),
stdout(Arrays.copyOfRange(annoyingData, 10, 11)),
stdout(Arrays.copyOfRange(annoyingData, 11, 12)),
stdout(Arrays.copyOfRange(annoyingData, 12, 13)),
stdout(Arrays.copyOfRange(annoyingData, 13, 14)));
final LoggingLogStreamFollower sut = LoggingLogStreamFollower.create(log);
sut.followLog(JobId.fromString("a:b:c"), "d", stream);
assertThat(appender.events(),
contains(event(Level.INFO, "[a] [d] 1 foo\u0000\uffff௵\uD808\uDC30")));
}
@Test
public void testPartialUtf8Interleaved() throws Exception {
final byte[] annoyingData = "foo\u0000\uffff௵\uD808\uDC30".getBytes(Charsets.UTF_8);
assertThat(annoyingData.length, is(14));
final Iterator<LogMessage> stream =
stream(stdout(Arrays.copyOfRange(annoyingData, 0, 1)),
stderr(Arrays.copyOfRange(annoyingData, 0, 1)),
stdout(Arrays.copyOfRange(annoyingData, 1, 2)),
stderr(Arrays.copyOfRange(annoyingData, 1, 2)),
stdout(Arrays.copyOfRange(annoyingData, 2, 3)),
stderr(Arrays.copyOfRange(annoyingData, 2, 3)),
stdout(Arrays.copyOfRange(annoyingData, 3, 4)),
stderr(Arrays.copyOfRange(annoyingData, 3, 4)),
stdout(Arrays.copyOfRange(annoyingData, 4, 5)),
stderr(Arrays.copyOfRange(annoyingData, 4, 5)),
stdout(Arrays.copyOfRange(annoyingData, 5, 6)),
stderr(Arrays.copyOfRange(annoyingData, 5, 6)),
stdout(Arrays.copyOfRange(annoyingData, 6, 7)),
stderr(Arrays.copyOfRange(annoyingData, 6, 7)),
stdout(Arrays.copyOfRange(annoyingData, 7, 8)),
stderr(Arrays.copyOfRange(annoyingData, 7, 8)),
stdout(Arrays.copyOfRange(annoyingData, 8, 9)),
stderr(Arrays.copyOfRange(annoyingData, 8, 9)),
stdout(Arrays.copyOfRange(annoyingData, 9, 10)),
stderr(Arrays.copyOfRange(annoyingData, 9, 10)),
stdout(Arrays.copyOfRange(annoyingData, 10, 11)),
stderr(Arrays.copyOfRange(annoyingData, 10, 11)),
stdout(Arrays.copyOfRange(annoyingData, 11, 12)),
stderr(Arrays.copyOfRange(annoyingData, 11, 12)),
stdout(Arrays.copyOfRange(annoyingData, 12, 13)),
stderr(Arrays.copyOfRange(annoyingData, 12, 13)),
stdout(Arrays.copyOfRange(annoyingData, 13, 14)),
stderr(Arrays.copyOfRange(annoyingData, 13, 14)));
final LoggingLogStreamFollower sut = LoggingLogStreamFollower.create(log);
sut.followLog(JobId.fromString("a:b:c"), "d", stream);
assertThat(appender.events(),
contains(event(Level.INFO, "[a] [d] 1 f"),
event(Level.INFO, "[a] [d] 2 f"),
event(Level.INFO, "[a] [d] 1 o"),
event(Level.INFO, "[a] [d] 2 o"),
event(Level.INFO, "[a] [d] 1 o"),
event(Level.INFO, "[a] [d] 2 o"),
event(Level.INFO, "[a] [d] 1 \u0000"),
event(Level.INFO, "[a] [d] 2 \u0000"),
event(Level.INFO, "[a] [d] 1 \uffff"),
event(Level.INFO, "[a] [d] 2 \uffff"),
event(Level.INFO, "[a] [d] 1 ௵"),
event(Level.INFO, "[a] [d] 2 ௵"),
event(Level.INFO, "[a] [d] 1 \uD808\uDC30"),
event(Level.INFO, "[a] [d] 2 \uD808\uDC30")));
}
@Test
public void testDropPartialUtf8Sequence() throws Exception {
final byte[] annoyingData = "௵".getBytes(Charsets.UTF_8);
assertThat(annoyingData.length, is(3));
final Iterator<LogMessage> stream =
stream(stdout(Arrays.copyOfRange(annoyingData, 0, 1)));
final LoggingLogStreamFollower sut = LoggingLogStreamFollower.create(log);
sut.followLog(JobId.fromString("a:b:c"), "d", stream);
assertThat(appender.events(), is(emptyCollectionOf(ILoggingEvent.class)));
}
@Test
public void testTruncateContainerId() throws Exception {
final Iterator<LogMessage> stream = stream(stdout("x"));
final LoggingLogStreamFollower sut = LoggingLogStreamFollower.create(log);
sut.followLog(JobId.fromString("a:b:c"), "0123456789abcdef", stream);
assertThat(appender.events(), contains(event(Level.INFO, "[a] [0123456] 1 x")));
}
private static Matcher<ILoggingEvent> event(Level level, String formattedMessage) {
return allOf(
hasProperty("level", is(level)),
hasProperty("formattedMessage", is(formattedMessage))
);
}
private static Iterator<LogMessage> stream(LogMessage... messages) {
return ImmutableList.copyOf(messages).iterator();
}
private static LogMessage stdout(String chunk) {
return message(LogMessage.Stream.STDOUT, chunk);
}
private static LogMessage stdout(byte[] chunk) {
return message(LogMessage.Stream.STDOUT, chunk);
}
private static LogMessage stderr(byte[] chunk) {
return message(LogMessage.Stream.STDERR, chunk);
}
private static LogMessage stderr(String chunk) {
return message(LogMessage.Stream.STDERR, chunk);
}
private static LogMessage message(
final LogMessage.Stream stream, final String chunk) {
return message(stream, chunk.getBytes(Charsets.UTF_8));
}
private static LogMessage message(
final LogMessage.Stream stream, final byte[] chunk) {
final ByteBuffer buffer = ByteBuffer.wrap(chunk);
return new LogMessage(stream, buffer.asReadOnlyBuffer());
}
}