/**
* Logback: the reliable, generic, fast and flexible logging framework.
* Copyright (C) 1999-2013, QOS.ch. All rights reserved.
* <p>
* This program and the accompanying materials are dual-licensed under
* either the terms of the Eclipse Public License v1.0 as published by
* the Eclipse Foundation
* <p>
* or (per the licensee's choosing)
* <p>
* under the terms of the GNU Lesser General Public License version 2.1
* as published by the Free Software Foundation.
*/
package ch.qos.logback.classic.android;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.slf4j.Logger;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.status.ErrorStatus;
import ch.qos.logback.core.status.StatusUtil;
import ch.qos.logback.core.util.CoreTestConstants;
import ch.qos.logback.core.util.Duration;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(RobolectricTestRunner.class)
public class SQLiteAppenderTest {
private static final String TEST_SQLITE_FILENAME = CoreTestConstants.OUTPUT_DIR_PREFIX + "SQLiteAppenderTest/logback.db";
private static final long EXPIRY_MS = 500;
private static final long NO_EXPIRY = 0;
private static final String SQLITE_APPENDER_WITHOUT_MAX_HISTORY_XML =
"<configuration>" +
"<appender name='db' class='ch.qos.logback.classic.android.SQLiteAppender'>" +
"<filename>" + TEST_SQLITE_FILENAME + "</filename>" +
"</appender>" +
"<root level='DEBUG'>" +
"<appender-ref ref='db' />" +
"</root>" +
"</configuration>";
private static final String SQLITE_APPENDER_WITH_MAX_HISTORY_XML =
"<configuration>" +
"<appender name='db' class='ch.qos.logback.classic.android.SQLiteAppender'>" +
"<filename>" + TEST_SQLITE_FILENAME + "</filename>" +
"<maxHistory>500 milli</maxHistory>" +
"</appender>" +
"<root level='DEBUG'>" +
"<appender-ref ref='db' />" +
"</root>" +
"</configuration>";
@Rule
public TemporaryFolder tmp = new TemporaryFolder();
private SQLiteLogCleaner logCleaner;
private long startTimeMs;
private LoggerContext context;
private SQLiteAppender appender;
@Before
public void setup() throws Exception {
context = new LoggerContext();
context.putProperty(CoreConstants.PACKAGE_NAME_KEY, "com.example");
appender = new SQLiteAppender();
appender.setFilename(TEST_SQLITE_FILENAME);
appender.setContext(context);
logCleaner = mock(SQLiteLogCleaner.class);
}
@After
public void teardown() {
new File(TEST_SQLITE_FILENAME).delete();
}
@Test
public void maxHistoryRemovesExpiredLogs() throws InterruptedException, JoranException {
configureLogbackByString(SQLITE_APPENDER_WITH_MAX_HISTORY_XML);
final int count = 12;
addLogEvents(count, EXPIRY_MS / 2);
Cursor c = getCursor();
assertRowCount(c, 0, count);
final int colIndex = c.getColumnIndex("timestmp");
while (c.moveToNext()) {
final long timestamp = c.getLong(colIndex);
assertThat(timestamp, is(greaterThan(startTimeMs - EXPIRY_MS)));
}
}
@Test
public void maxHistoryIsDisabledByDefault() throws InterruptedException, JoranException {
configureLogbackByString(SQLITE_APPENDER_WITHOUT_MAX_HISTORY_XML);
final int count = 1000;
addLogEvents(count, NO_EXPIRY);
assertThat(getCursor().getCount(), is(count));
}
@Test
public void cleanuOccursAtAppenderStartup() throws InterruptedException {
addAppenderToContext("1 hour");
verify(logCleaner, times(1)).performLogCleanup(any(SQLiteDatabase.class), any(Duration.class));
}
@Test
public void cleanupDoesNotOccurBeforeExpiration() throws InterruptedException {
addAppenderToContext("1 hour");
addLogEvents(3, NO_EXPIRY);
// log-cleanup normally called between logging events if expiry time
// exceeded, but no expiration here, so call-count should still be 1
verify(logCleaner, times(1)).performLogCleanup(any(SQLiteDatabase.class), any(Duration.class));
}
@Test
public void cleanupOccursAfterEveryExpiration() throws InterruptedException {
addAppenderToContext(EXPIRY_MS + " milli");
final int count = 7;
final long delayMs = EXPIRY_MS / 2;
final int expectedCallCount = (int)Math.ceil((double)(delayMs * count)/EXPIRY_MS);
addLogEvents(count, delayMs);
verify(logCleaner, times(expectedCallCount)).performLogCleanup(any(SQLiteDatabase.class), any(Duration.class));
}
@Test
public void dirAsFilenameResultsInDefault() throws IOException {
final File file = appender.getDatabaseFile(tmp.newFolder().getAbsolutePath());
assertThat(file, is(notNullValue()));
assertThat(file.getName(), is("logback.db"));
}
@Test
public void nullFilenameResultsInDefault() throws IOException {
final File file = appender.getDatabaseFile(null);
assertThat(file, is(notNullValue()));
assertThat(file.getName(), is("logback.db"));
}
@Test
public void emptyFilenameResultsInDefault() throws IOException {
final File file = appender.getDatabaseFile("");
assertThat(file, is(notNullValue()));
assertThat(file.getName(), is("logback.db"));
}
@Test
public void blankFilenameResultsInDefault() throws IOException {
final File file = appender.getDatabaseFile(" ");
assertThat(file, is(notNullValue()));
assertThat(file.getName(), is("logback.db"));
}
@Test
public void setsDatabaseFilename() throws IOException {
final File tmpFile = tmp.newFile();
final File file = appender.getDatabaseFile(tmpFile.getAbsolutePath());
assertThat(file, is(notNullValue()));
assertThat(file.getName(), is(tmpFile.getName()));
}
@Test
public void getMaxHistoryReturnsOriginalSetting() {
// note that Duration.toString() returns units in "milliseconds",
// "seconds", "minutes", or "hours"
appender.setMaxHistory("800 milli");
assertThat(appender.getMaxHistory(), containsString("800 milli"));
appender.setMaxHistory("500 seconds");
assertThat(appender.getMaxHistory(), containsString("8 minutes"));
appender.setMaxHistory("120 minutes");
assertThat(appender.getMaxHistory(), containsString("2 hours"));
appender.setMaxHistory("1 hour");
assertThat(appender.getMaxHistory(), containsString("1 hour"));
appender.setMaxHistory("7 days");
assertThat(appender.getMaxHistory(), containsString("168 hours"));
}
@Test
public void maxHistorySetsMilliseconds() {
appender.setMaxHistory("800 milli");
assertThat(appender.getMaxHistoryMs(), is(800L));
appender.setMaxHistory("500 seconds");
assertThat(appender.getMaxHistoryMs(), is(500 * 1000L));
appender.setMaxHistory("120 minutes");
assertThat(appender.getMaxHistoryMs(), is(120 * 60 * 1000L));
appender.setMaxHistory("1 hour");
assertThat(appender.getMaxHistoryMs(), is(60 * 60 * 1000L));
appender.setMaxHistory("7 days");
assertThat(appender.getMaxHistoryMs(), is(7 * 24 * 60 * 60 * 1000L));
}
@Test
public void getMaxHistoryEmptyByDefault() {
assertThat(appender.getMaxHistory(), is(""));
}
@Test
public void getMaxHistoryMsZeroByDefault() {
assertThat(appender.getMaxHistoryMs(), is(0L));
}
private void configureLogbackByString(String xml) throws JoranException {
JoranConfigurator config = new JoranConfigurator();
config.setContext(context);
InputStream stream = new ByteArrayInputStream(xml.getBytes());
config.doConfigure(stream);
StatusUtil statusUtil = new StatusUtil(context);
if (statusUtil.getHighestLevel(0) == ErrorStatus.ERROR) {
fail("Configuration error found");
}
}
private void addLogEvents(int count, long delayMs) throws InterruptedException {
Logger log = context.getLogger(SQLiteAppenderTest.class);
startTimeMs = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
log.info("i={}", i);
if (delayMs > 0) {
Thread.sleep(delayMs);
}
}
}
/** Gets a SQLiteAppender with a no-op log-cleaner mock */
private void addAppenderToContext(String maxHistory) {
appender.setMaxHistory(maxHistory);
appender.setLogCleaner(logCleaner);
appender.start();
ch.qos.logback.classic.Logger testRoot = context.getLogger(SQLiteAppenderTest.class);
testRoot.addAppender(appender);
}
private Cursor getCursor() {
SQLiteDatabase db = SQLiteDatabase.openDatabase(TEST_SQLITE_FILENAME, null, SQLiteDatabase.OPEN_READONLY);
return db.rawQuery("SELECT timestmp FROM logging_event;", null);
}
private void assertRowCount(Cursor c, int min, int max) {
assertThat(c.getCount(), is(greaterThan(min)));
assertThat(c.getCount(), is(lessThan(max)));
}
}