/* * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. * * 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.github.susom.database.test; import java.io.File; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; import java.util.List; import org.apache.log4j.AppenderSkeleton; import org.apache.log4j.Level; import org.apache.log4j.LogManager; import org.apache.log4j.PatternLayout; import org.apache.log4j.spi.LoggingEvent; import org.apache.log4j.xml.DOMConfigurator; import org.easymock.IMocksControl; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import com.github.susom.database.Database; import com.github.susom.database.DatabaseException; import com.github.susom.database.DatabaseImpl; import com.github.susom.database.DatabaseMock; import com.github.susom.database.DatabaseProvider; import com.github.susom.database.DebugSql; import com.github.susom.database.Flavor; import com.github.susom.database.OptionsDefault; import com.github.susom.database.OptionsOverride; import static org.easymock.EasyMock.*; import static org.junit.Assert.*; import static org.hamcrest.core.StringContains.containsString; /** * Unit tests for the Database and Sql implementation classes. * * @author garricko */ @RunWith(JUnit4.class) @SuppressWarnings("ReturnValueIgnored") public class DatabaseTest { static { // Initialize logging String log4jConfig = new File("log4j.xml").getAbsolutePath(); DOMConfigurator.configure(log4jConfig); org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(DatabaseTest.class); log.info("Initialized log4j using file: " + log4jConfig); } private OptionsDefault options = new OptionsDefault(Flavor.postgresql) { int errors = 0; @Override public String generateErrorCode() { errors++; return Integer.toString(errors); } }; private OptionsOverride optionsFullLog = new OptionsOverride(options) { @Override public boolean isLogParameters() { return true; } }; private LogCaptureAppender capturedLog; @Before public void initLogCapture() { capturedLog = new LogCaptureAppender(); capturedLog.setThreshold(Level.DEBUG); LogManager.getRootLogger().addAppender(capturedLog); } @After public void stopLogCapture() { LogManager.getRootLogger().removeAppender(capturedLog); } @Test public void staticSqlToLong() throws Exception { Connection c = createNiceMock(Connection.class); PreparedStatement ps = createNiceMock(PreparedStatement.class); ResultSet rs = createNiceMock(ResultSet.class); expect(c.prepareStatement("select 1 from dual")).andReturn(ps); expect(ps.executeQuery()).andReturn(rs); expect(rs.next()).andReturn(true); expect(rs.getLong(1)).andReturn(1L); expect(rs.wasNull()).andReturn(false); replay(c, ps, rs); assertEquals(new Long(1), new DatabaseImpl(c, options).toSelect("select 1 from dual").queryLongOrNull()); verify(c, ps, rs); } @Test public void when() throws Exception { Database db = new DatabaseImpl(createNiceMock(Connection.class), new OptionsDefault(Flavor.oracle)); assertEquals("oracle", "" + db.when().oracle("oracle")); assertEquals("oracle", db.when().oracle("oracle").other("")); assertEquals("oracle", db.when().derby("derby").oracle("oracle").other("other")); assertEquals("", db.when().derby("derby").other("")); } /** * Verify the default options cause exceptions to be thrown when calling commitNow() and * rollbackNow(). */ @Test public void explicitTransactionControlDisabled() { Database db = new DatabaseImpl(createNiceMock(Connection.class), new OptionsDefault(Flavor.oracle)); try { db.commitNow(); fail("Should have thrown an exception"); } catch (DatabaseException e) { assertThat(e.getMessage(), containsString("Calls to commitNow() are not allowed")); } try { db.rollbackNow(); fail("Should have thrown an exception"); } catch (DatabaseException e) { assertThat(e.getMessage(), containsString("Calls to rollbackNow() are not allowed")); } } /** * Verify custom options with allowTransactionControl() == true cause the commitNow() * and rollbackNow() methods to call the underlying methods on the Connection class. */ @Test public void explicitTransactionControlEnabled() throws Exception { Connection c = createNiceMock(Connection.class); c.commit(); c.rollback(); replay(c); Database db = new DatabaseImpl(c, new OptionsDefault(Flavor.oracle) { @Override public boolean allowTransactionControl() { return true; } }); db.commitNow(); db.rollbackNow(); verify(c); } @Test public void underlyingConnection() { Connection c = createNiceMock(Connection.class); try { new DatabaseImpl(c, new OptionsDefault(Flavor.derby)).underlyingConnection(); fail("Should have thrown an exception"); } catch (DatabaseException e) { assertEquals("Calls to underlyingConnection() are not allowed", e.getMessage()); } assertTrue(c == new DatabaseImpl(c, new OptionsOverride() { @Override public boolean allowConnectionAccess() { return true; } }).underlyingConnection()); } @Test public void staticSqlToLongNoRows() throws Exception { Connection c = createNiceMock(Connection.class); PreparedStatement ps = createNiceMock(PreparedStatement.class); ResultSet rs = createNiceMock(ResultSet.class); expect(c.prepareStatement("select * from dual")).andReturn(ps); expect(ps.executeQuery()).andReturn(rs); expect(rs.next()).andReturn(false); replay(c, ps, rs); assertNull(new DatabaseImpl(c, options).toSelect("select * from dual").queryLongOrNull()); verify(c, ps, rs); } @Test public void staticSqlToLongNullValue() throws Exception { Connection c = createNiceMock(Connection.class); PreparedStatement ps = createNiceMock(PreparedStatement.class); ResultSet rs = createNiceMock(ResultSet.class); expect(c.prepareStatement("select null from dual")).andReturn(ps); expect(ps.executeQuery()).andReturn(rs); expect(rs.next()).andReturn(true); expect(rs.getLong(1)).andReturn(0L); expect(rs.wasNull()).andReturn(true); replay(c, ps, rs); assertNull(new DatabaseImpl(c, options).toSelect("select null from dual").queryLongOrNull()); verify(c, ps, rs); } @Test public void sqlArgLongPositional() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); PreparedStatement ps = control.createMock(PreparedStatement.class); ResultSet rs = control.createMock(ResultSet.class); expect(c.prepareStatement("select a from b where c=?")).andReturn(ps); ps.setObject(eq(1), eq(new Long(1))); expect(ps.executeQuery()).andReturn(rs); expect(rs.next()).andReturn(false); rs.close(); ps.close(); control.replay(); assertNull(new DatabaseImpl(c, options).toSelect("select a from b where c=?").argLong(1L).queryLongOrNull()); control.verify(); } @Test public void sqlArgLongNull() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); PreparedStatement ps = control.createMock(PreparedStatement.class); ResultSet rs = control.createMock(ResultSet.class); expect(c.prepareStatement("select a from b where c=?")).andReturn(ps); ps.setNull(eq(1), eq(Types.NUMERIC)); expect(ps.executeQuery()).andReturn(rs); expect(rs.next()).andReturn(false); rs.close(); ps.close(); control.replay(); assertNull(new DatabaseImpl(c, options).toSelect("select a from b where c=?").argLong(null).queryLongOrNull()); control.verify(); } @Test public void settingTimeout() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); PreparedStatement ps = control.createMock(PreparedStatement.class); ResultSet rs = control.createMock(ResultSet.class); expect(c.prepareStatement("select a from b")).andReturn(ps); ps.setQueryTimeout(21); expect(ps.executeQuery()).andReturn(rs); expect(rs.next()).andReturn(false); rs.close(); ps.close(); control.replay(); assertNull(new DatabaseImpl(c, options).toSelect("select a from b").withTimeoutSeconds(21).queryLongOrNull()); control.verify(); } @Test public void settingMaxRows() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); PreparedStatement ps = control.createMock(PreparedStatement.class); ResultSet rs = control.createMock(ResultSet.class); expect(c.prepareStatement("select a from b")).andReturn(ps); ps.setMaxRows(15); expect(ps.executeQuery()).andReturn(rs); expect(rs.next()).andReturn(false); rs.close(); ps.close(); control.replay(); assertNull(new DatabaseImpl(c, options).toSelect("select a from b").withMaxRows(15).queryLongOrNull()); control.verify(); } @Test public void sqlArgLongNamed() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); PreparedStatement ps = control.createMock(PreparedStatement.class); ResultSet rs = control.createMock(ResultSet.class); expect(c.prepareStatement("select ':a' from b where c=?")).andReturn(ps); ps.setObject(eq(1), eq(new Long(1))); expect(ps.executeQuery()).andReturn(rs); expect(rs.next()).andReturn(false); rs.close(); ps.close(); control.replay(); assertNull(new DatabaseImpl(c, options).toSelect("select '::a' from b where c=:c").argLong("c", 1L).queryLongOrNull()); control.verify(); } @Test @Retry public void logFormatNoDebugSql() throws Exception { System.out.println(new DatabaseImpl(createNiceMock(DatabaseMock.class), options) .toSelect("select a from b where c=?") .argInteger(1) .queryLongOrNull()); capturedLog.assertNoWarningsOrErrors(); assertTrue(capturedLog.messages().get(0).endsWith("\tselect a from b where c=?")); } @Test public void logFormatDebugSqlInteger() throws Exception { System.out.println(new DatabaseImpl(createNiceMock(DatabaseMock.class), optionsFullLog) .toSelect("select a from b where c=?") .argInteger(1) .queryLongOrNull()); capturedLog.assertNoWarningsOrErrors(); capturedLog.assertMessage(Level.DEBUG, "Query: ${timing}\tselect a from b where c=?${sep}select a from b where c=1"); } @Test public void doNotLogSecrets() throws Exception { System.out.println(new DatabaseImpl(createNiceMock(DatabaseMock.class), optionsFullLog) .toSelect("select a from b where c=:secret_arg") .argInteger("secret_arg", 1) .queryLongOrNull()); capturedLog.assertNoWarningsOrErrors(); capturedLog.assertMessage(Level.DEBUG, "Query: ${timing}\tselect a from b where c=?${sep}select a from b where c=<secret>"); } @Test public void missingPositionalParameter() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); control.replay(); try { Long value = new DatabaseImpl(c, new OptionsDefault(Flavor.postgresql) { int errors = 0; @Override public String generateErrorCode() { errors++; return Integer.toString(errors); } }).toSelect("select a from b where c=?").queryLongOrNull(); fail("Should have thrown an exception, but returned " + value); } catch (DatabaseException e) { assertEquals("Error executing SQL (errorCode=1)", e.getMessage()); } control.verify(); } @Test public void extraPositionalParameter() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); control.replay(); try { Long value = new DatabaseImpl(c, new OptionsDefault(Flavor.postgresql) { int errors = 0; @Override public boolean isDetailedExceptions() { return true; } @Override public boolean isLogParameters() { return true; } @Override public String generateErrorCode() { errors++; return Integer.toString(errors); } }).toSelect("select a from b where c=?").argString("hi").argInteger(1).queryLongOrNull(); fail("Should have thrown an exception but returned " + value); } catch (DatabaseException e) { assertEquals("Error executing SQL (errorCode=1): (wrong # args) query: select a from b where c=?", e.getMessage()); } control.verify(); } @Test public void missingNamedParameter() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); control.replay(); try { Long value = new DatabaseImpl(c, new OptionsDefault(Flavor.postgresql) { int errors = 0; @Override public boolean isDetailedExceptions() { return true; } @Override public boolean isLogParameters() { return true; } @Override public String generateErrorCode() { errors++; return Integer.toString(errors); } }).toSelect("select a from b where c=:x").queryLongOrNull(); fail("Should have thrown an exception but returned " + value); } catch (DatabaseException e) { assertEquals("Error executing SQL (errorCode=1): select a from b where c=:x", e.getMessage()); } control.verify(); } @Test public void missingNamedParameter2() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); control.replay(); try { Long value = new DatabaseImpl(c, options).toSelect("select a from b where c=:x and d=:y") .argString("x", "hi").queryLongOrNull(); fail("Should have thrown an exception but returned " + value); } catch (DatabaseException e) { assertEquals("Error executing SQL (errorCode=1)", e.getMessage()); } control.verify(); } @Test public void extraNamedParameter() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); control.replay(); try { Long value = new DatabaseImpl(c, options).toSelect("select a from b where c=:x") .argString("x", "hi").argString("y", "bye").queryLongOrNull(); fail("Should have thrown an exception but returned " + value); } catch (DatabaseException e) { assertEquals("Error executing SQL (errorCode=1)", e.getMessage()); } control.verify(); } @Test public void mixedParameterTypes() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); PreparedStatement ps = control.createMock(PreparedStatement.class); ResultSet rs = control.createMock(ResultSet.class); expect(c.prepareStatement("select a from b where c=? and d=?")).andReturn(ps); ps.setObject(eq(1), eq("bye")); ps.setNull(eq(2), eq(Types.TIMESTAMP)); expect(ps.executeQuery()).andReturn(rs); expect(rs.next()).andReturn(false); rs.close(); ps.close(); control.replay(); assertNull(new DatabaseImpl(c, options).toSelect("select a from b where c=:x and d=?") .argString(":x", "bye").argDate(null).queryLongOrNull()); control.verify(); } @Test public void mixedParameterTypesReversed() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); PreparedStatement ps = control.createMock(PreparedStatement.class); ResultSet rs = control.createMock(ResultSet.class); expect(c.prepareStatement("select a from b where c=? and d=?")).andReturn(ps); ps.setObject(eq(1), eq("bye")); ps.setNull(eq(2), eq(Types.TIMESTAMP)); expect(ps.executeQuery()).andReturn(rs); expect(rs.next()).andReturn(false); rs.close(); ps.close(); control.replay(); // Reverse order of args should be the same assertNull(new DatabaseImpl(c, options).toSelect("select a from b where c=:x and d=?") .argDate(null).argString(":x", "bye").queryLongOrNull()); control.verify(); } @Test public void wrongNumberOfInserts() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); PreparedStatement ps = control.createMock(PreparedStatement.class); expect(c.prepareStatement("insert into x (y) values (1)")).andReturn(ps); expect(ps.executeUpdate()).andReturn(2); ps.close(); control.replay(); try { new DatabaseImpl(c, options).toInsert("insert into x (y) values (1)").insert(1); fail("Should have thrown an exception"); } catch (DatabaseException e) { assertThat(e.getMessage(), containsString("The number of affected rows was 2, but 1 were expected.")); } control.verify(); } @Test public void cancelQuery() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); PreparedStatement ps = control.createMock(PreparedStatement.class); expect(c.prepareStatement("select a from b")).andReturn(ps); expect(ps.executeQuery()).andThrow(new SQLException("Cancelled", "cancel", 1013)); ps.close(); control.replay(); try { Long value = new DatabaseImpl(c, options).toSelect("select a from b").queryLongOrNull(); fail("Should have thrown an exception but returned " + value); } catch (DatabaseException e) { assertEquals("Timeout of -1 seconds exceeded or user cancelled", e.getMessage()); } control.verify(); } @Test public void otherException() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); PreparedStatement ps = control.createMock(PreparedStatement.class); expect(c.prepareStatement("select a from b")).andReturn(ps); expect(ps.executeQuery()).andThrow(new RuntimeException("Oops")); ps.close(); control.replay(); try { Long value = new DatabaseImpl(c, new OptionsDefault(Flavor.postgresql) { int errors = 0; @Override public boolean isDetailedExceptions() { return true; } @Override public boolean isLogParameters() { return true; } @Override public String generateErrorCode() { errors++; return Integer.toString(errors); } }).toSelect("select a from b").queryLongOrNull(); fail("Should have thrown an exception but returned " + value); } catch (DatabaseException e) { assertEquals("Error executing SQL (errorCode=1): select a from b", e.getMessage()); } control.verify(); } @Test public void closeExceptions() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); PreparedStatement ps = control.createMock(PreparedStatement.class); ResultSet rs = control.createMock(ResultSet.class); expect(c.prepareStatement("select a from b")).andReturn(ps); expect(ps.executeQuery()).andReturn(rs); expect(rs.next()).andThrow(new RuntimeException("Primary")); rs.close(); expectLastCall().andThrow(new RuntimeException("Oops1")); ps.close(); expectLastCall().andThrow(new RuntimeException("Oops2")); control.replay(); try { Long value = new DatabaseImpl(c, new OptionsDefault(Flavor.postgresql) { int errors = 0; @Override public boolean isDetailedExceptions() { return true; } @Override public boolean isLogParameters() { return true; } @Override public String generateErrorCode() { errors++; return Integer.toString(errors); } }).toSelect("select a from b").queryLongOrNull(); fail("Should have thrown an exception but returned " + value); } catch (DatabaseException e) { assertEquals("Error executing SQL (errorCode=1): select a from b", e.getMessage()); } control.verify(); } @Test public void transactionsNotAllowed() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); control.replay(); try { new DatabaseImpl(c, options).commitNow(); fail("Should have thrown an exception"); } catch (DatabaseException e) { assertEquals("Calls to commitNow() are not allowed", e.getMessage()); } try { new DatabaseImpl(c, options).rollbackNow(); fail("Should have thrown an exception"); } catch (DatabaseException e) { assertEquals("Calls to rollbackNow() are not allowed", e.getMessage()); } control.verify(); } @Test public void transactionCommitSuccess() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); c.commit(); control.replay(); new DatabaseImpl(c, new OptionsDefault(Flavor.postgresql) { @Override public boolean allowTransactionControl() { return true; } }).commitNow(); control.verify(); } @Test public void transactionCommitFail() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); c.commit(); expectLastCall().andThrow(new SQLException("Oops")); control.replay(); try { new DatabaseImpl(c, new OptionsDefault(Flavor.postgresql) { @Override public boolean allowTransactionControl() { return true; } }).commitNow(); fail("Should have thrown an exception"); } catch (DatabaseException e) { assertEquals("Unable to commit transaction", e.getMessage()); } control.verify(); } @Test public void transactionRollbackSuccess() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); c.rollback(); control.replay(); new DatabaseImpl(c, new OptionsDefault(Flavor.postgresql) { @Override public boolean allowTransactionControl() { return true; } }).rollbackNow(); control.verify(); } @Test public void transactionRollbackFail() throws Exception { IMocksControl control = createStrictControl(); Connection c = control.createMock(Connection.class); c.rollback(); expectLastCall().andThrow(new SQLException("Oops")); control.replay(); try { new DatabaseImpl(c, new OptionsDefault(Flavor.postgresql) { @Override public boolean allowTransactionControl() { return true; } }).rollbackNow(); fail("Should have thrown an exception"); } catch (DatabaseException e) { assertEquals("Unable to rollback transaction", e.getMessage()); } control.verify(); } @Test public void transactCommitOnlyWithNoError() throws Exception { IMocksControl control = createStrictControl(); final Connection c = control.createMock(Connection.class); c.setAutoCommit(false); c.commit(); c.close(); control.replay(); new DatabaseProvider(() -> c, new OptionsDefault(Flavor.postgresql)).transact((db, tx) -> { tx.setRollbackOnError(false); db.get(); }); control.verify(); } @Test public void transactCommitOnlyWithError() throws Exception { IMocksControl control = createStrictControl(); final Connection c = control.createMock(Connection.class); c.setAutoCommit(false); c.commit(); c.close(); control.replay(); try { new DatabaseProvider(() -> c, new OptionsDefault(Flavor.postgresql)).transact((db, tx) -> { tx.setRollbackOnError(false); db.get(); throw new Error("Oops"); }); fail("Should have thrown an exception"); } catch (Exception e) { assertEquals("Error during transaction", e.getMessage()); } control.verify(); } @Test public void transactCommitOnlyOverrideWithError() throws Exception { IMocksControl control = createStrictControl(); final Connection c = control.createMock(Connection.class); c.setAutoCommit(false); c.rollback(); c.close(); control.replay(); try { new DatabaseProvider(() -> c, new OptionsDefault(Flavor.postgresql)).transact((db, tx) -> { db.get(); tx.setRollbackOnError(true); throw new DatabaseException("Oops"); }); fail("Should have thrown an exception"); } catch (Exception e) { assertEquals("Oops", e.getMessage()); } control.verify(); } @Test public void transactCommitOnlyOverrideWithError2() throws Exception { IMocksControl control = createStrictControl(); final Connection c = control.createMock(Connection.class); c.setAutoCommit(false); c.rollback(); c.close(); control.replay(); try { new DatabaseProvider(() -> c, new OptionsDefault(Flavor.postgresql)).transact((db, tx) -> { db.get(); tx.setRollbackOnError(false); tx.setRollbackOnly(true); throw new DatabaseException("Oops"); }); fail("Should have thrown an exception"); } catch (Exception e) { assertEquals("Oops", e.getMessage()); } control.verify(); } @Test public void transactRollbackOnly() throws Exception { IMocksControl control = createStrictControl(); final Connection c = control.createMock(Connection.class); c.setAutoCommit(false); c.rollback(); c.close(); control.replay(); new DatabaseProvider(() -> c, new OptionsDefault(Flavor.postgresql)).transact((db, tx) -> { db.get(); tx.setRollbackOnly(true); }); control.verify(); } @Test public void transactRollbackOnErrorWithError() throws Exception { IMocksControl control = createStrictControl(); final Connection c = control.createMock(Connection.class); c.setAutoCommit(false); c.rollback(); c.close(); control.replay(); try { new DatabaseProvider(() -> c, new OptionsDefault(Flavor.postgresql)).transact(db -> { db.get(); throw new Exception("Oops"); }); fail("Should have thrown an exception"); } catch (Exception e) { assertEquals("Error during transaction", e.getMessage()); } control.verify(); } @Test public void transactRollbackOnErrorWithNoError() throws Exception { IMocksControl control = createStrictControl(); final Connection c = control.createMock(Connection.class); c.setAutoCommit(false); c.commit(); c.close(); control.replay(); new DatabaseProvider(() -> c, new OptionsDefault(Flavor.postgresql)).transact((db) -> { db.get(); }); control.verify(); } private static class LogCaptureAppender extends AppenderSkeleton { private final List<LoggingEvent> events = new ArrayList<>(); private int warnings = 0; private int errors = 0; protected synchronized void append(LoggingEvent event) { if (Level.TRACE.isGreaterOrEqual(event.getLevel())) { return; } if (event.getLevel().equals(Level.WARN)) { warnings++; } else if (event.getLevel().isGreaterOrEqual(Level.ERROR)) { errors++; } events.add(event); } public synchronized int nbrWarnings() { return warnings; } public synchronized int nbrErrors() { return errors; } public void close() { // Nothing to do } public synchronized String toString() { PatternLayout layout = new PatternLayout("%-5p %c %m\u00AE%n"); StringBuilder builder = new StringBuilder(); for (LoggingEvent event : events) { builder.append(layout.format(event)); } return builder.toString(); } public synchronized List<String> messages() { List<String> messages = new ArrayList<>(); PatternLayout layout = new PatternLayout("%-5p %c %m"); for (LoggingEvent event : events) { messages.add(layout.format(event)); } return messages; } public synchronized void assertNoWarningsOrErrors() { assertEquals("Warnings or errors in the log:\n" + toString(), 0, nbrWarnings() + nbrErrors()); } public synchronized void assertWarnings(int nbrWarnings) { assertEquals("Wrong number of warnings in the log:\n" + toString(), nbrWarnings, nbrWarnings()); } public synchronized void assertErrors(int nbrErrors) { assertEquals("Wrong number of errors or above in the log:\n" + toString(), nbrErrors, nbrErrors()); } public synchronized void assertEntries(int nbrEntries) { assertEquals("Wrong number of entries in the log:\n" + toString(), nbrEntries, events.size()); } /** * Check for a log entry with a particular log level and message. * * <p>The message pattern is a literal, exactly matching string, * but it may contain a few special tokens that will match varying * input in the messages:</p> * * <ul> * <li>${timing} - this will match a typical section of metrics</li> * <li>${sep} - this will match the boundary between the raw SQL and * the debug sql that logs the parameters as well</li> * </ul> * * @param level the specific level we are looking for * @param messagePattern a search pattern (see notes above) */ public synchronized void assertMessage(Level level, String messagePattern) { boolean found = false; for (LoggingEvent event : events) { if (!event.getLevel().equals(level)) { continue; } String message = event.getRenderedMessage(); int messagePos = 0; int patternPos = 0; while (messagePos < message.length() && patternPos < messagePattern.length()) { // Advance until we find a character that doesn't match if (message.charAt(messagePos) == messagePattern.charAt(patternPos)) { messagePos++; patternPos++; continue; } // If message has literal '$' terminate because the pattern doesn't have a special matcher if (message.charAt(messagePos) == '$') { break; } // Special matchers: expressions for things that vary with each run if (messagePattern.startsWith("${timing}", patternPos) && message.indexOf(')', messagePos) != -1) { messagePos = message.indexOf(')', messagePos) + 1; patternPos += "${timing}".length(); continue; } if (messagePattern.startsWith("${sep}", patternPos) && message.startsWith(DebugSql.PARAM_SQL_SEPARATOR, messagePos)) { messagePos += DebugSql.PARAM_SQL_SEPARATOR.length(); patternPos += "${sep}".length(); continue; } // Couldn't match break; } if (messagePos >= message.length() && patternPos >= messagePattern.length()) { found = true; break; } } assertTrue("Log message not found (" + level + " " + messagePattern + ") in log:\n" + toString(), found); } public boolean requiresLayout() { return false; } } }