/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * See LICENSE.txt included in this distribution for the specific * language governing permissions and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at LICENSE.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright 2010 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ package org.opensolaris.opengrok.history; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; import org.opensolaris.opengrok.configuration.RuntimeEnvironment; import org.opensolaris.opengrok.util.Executor; import org.opensolaris.opengrok.util.TestRepository; /** * Unit tests for {@code JDBCHistoryCache}. */ public class JDBCHistoryCacheTest extends TestCase { private TestRepository repositories; private JDBCHistoryCache cache; private static Boolean hasDbtools; private static final String DERBY_EMBEDDED_DRIVER = "org.apache.derby.jdbc.EmbeddedDriver"; @SuppressWarnings("javadoc") public JDBCHistoryCacheTest(String name) { super(name); } /** * Create a suite of tests to run. If the Derby classes are not present, * skip this test. * * @return tests to run */ public static Test suite() { try { Class.forName(DERBY_EMBEDDED_DRIVER); return new TestSuite(JDBCHistoryCacheTest.class); } catch (ClassNotFoundException e) { return new TestSuite("JDBCHistoryCacheTest - empty (no derby.jar)"); } } @SuppressWarnings("unused") private void dumpDB(String prefix) { String file = "/tmp/opengrok:" + prefix; File f = new File(file + ".sql"); if (hasDbtools == null || hasDbtools.booleanValue()) { Class clazz = null; try { clazz = Class.forName("org.apache.derby.tools.dblook"); Method main = clazz.getMethod("main", String[].class); if (f.exists()) { f.delete(); } String[] args = new String[] {"-d", getURL(), "-o", f.toString()}; main.invoke(clazz, (Object) args); System.out.println("Wrote " + f); } catch (ClassNotFoundException e) { // do nothing } catch (SecurityException e) { // ignore } catch (NoSuchMethodException e) { // ignore } catch (IllegalArgumentException e) { // ignore } catch (IllegalAccessException e) { // ignore } catch (InvocationTargetException e) { // ignore } hasDbtools = Boolean.valueOf(clazz != null); } Connection conn = null; Statement stmt = null; ResultSet rs = null; PreparedStatement ps = null; ArrayList tables = new ArrayList(); String schema = "OPENGROK"; try { conn = DriverManager.getConnection(getURL()); stmt = conn.createStatement(); stmt.executeUpdate("SET SCHEMA SYS"); rs = stmt.executeQuery("SELECT T.TABLEID, T.TABLENAME, " + "S.SCHEMANAME FROM SYS.SYSTABLES T, SYS.SYSSCHEMAS S " + "WHERE T.TABLETYPE = 'T' AND T.SCHEMAID = S.SCHEMAID"); while (rs.next()) { String tableName = rs.getString(2); String schemaName = rs.getString(3); if (schemaName.equals(schema)) { tables.add(tableName); } } ps = conn.prepareStatement("CALL SYSCS_UTIL.SYSCS_EXPORT_TABLE " + "(?,?,?,?,?,?)"); for (String table : tables) { f = new File(file + "_" + table + ".dat"); if (f.exists()) { f.delete(); } ps.setString(1, schema); ps.setString(2, table); ps.setString(3, f.toString()); ps.setString(4, "|"); ps.setString(5, null); ps.setString(6, null); ps.execute(); System.out.println("Wrote " + f); } } catch (SQLException e) { System.out.println(e.getLocalizedMessage()); } finally { try { if (stmt != null) stmt.close(); } catch (SQLException e) { // ignore } try { if (ps != null) ps.close(); } catch (SQLException e) { // ignore } try { if (conn != null) conn.close(); } catch (SQLException e) { // ignore } } } /** * Set up the test environment with repositories and a cache instance. */ @Override protected void setUp() throws Exception { repositories = new TestRepository(); repositories.create(getClass().getResourceAsStream("repositories.zip")); cache = new JDBCHistoryCache( DERBY_EMBEDDED_DRIVER, getURL() + ";create=true"); cache.initialize(); } /** * Clean up after the test. Remove the test repositories and shut down * the database. */ @Override protected void tearDown() throws Exception { repositories.destroy(); repositories = null; cache = null; try { DriverManager.getConnection(getURL() + ";shutdown=true"); } catch (SQLException sqle) { // Expect SQLException with SQLState 08006 on successful shutdown if (!sqle.getSQLState().equals("08006")) { throw sqle; } } } /** * Create a database URL to use for this test. The URL points to an * in-memory Derby database. * * @return a database URL */ private String getURL() { return "jdbc:derby:memory:DB-" + getName(); } /** * Import a new changeset into a Mercurial repository. * * @param reposRoot the root of the repository * @param changesetFile file that contains the changeset to import */ private static void importHgChangeset(File reposRoot, String changesetFile) { String[] cmdargs = { MercurialRepository.CMD_FALLBACK, "import", changesetFile }; Executor exec = new Executor(Arrays.asList(cmdargs), reposRoot); int exitCode = exec.exec(); if (exitCode != 0) { fail("hg import failed." + "\nexit code: " + exitCode + "\nstdout:\n" + exec.getOutputString() + "\nstderr:\n" + exec.getErrorString()); } } /** * Assert that two HistoryEntry objects are equal. * @param expected the expected entry * @param actual the actual entry * @throws AssertFailure if the two entries don't match */ private static void assertSameEntries( List expected, List actual) { assertEquals("Unexpected size", expected.size(), actual.size()); Iterator actualIt = actual.iterator(); for (HistoryEntry expectedEntry : expected) { assertSameEntry(expectedEntry, actualIt.next()); } assertFalse("More entries than expected", actualIt.hasNext()); } /** * Assert that two lists of HistoryEntry objects are equal. * @param expected the expected list of entries * @param actual the actual list of entries * @throws AssertFailure if the two lists don't match */ private static void assertSameEntry(HistoryEntry expected, HistoryEntry actual) { assertEquals(expected.getAuthor(), actual.getAuthor()); assertEquals(expected.getRevision(), actual.getRevision()); assertEquals(expected.getDate(), actual.getDate()); assertEquals(expected.getMessage(), actual.getMessage()); assertEquals(expected.getFiles(), actual.getFiles()); } /** * Basic tests for the {@code store()} and {@code get()} methods. * @throws Exception */ @SuppressWarnings("boxing") public void testStoreAndGet() throws Exception { File reposRoot = new File(repositories.getSourceRoot(), "mercurial"); Repository repos = RepositoryFactory.getRepository(reposRoot); History historyToStore = repos.getHistory(reposRoot); cache.store(historyToStore, repos); cache.optimize(); // test get history for single file File makefile = new File(reposRoot, "Makefile"); assertTrue(makefile.exists()); History retrievedHistory = cache.get(makefile, repos, true, null); History retrievedHistory2 = cache.get(makefile, repos, true, Boolean.FALSE); assertSameEntries(retrievedHistory.getHistoryEntries(), retrievedHistory2.getHistoryEntries()); retrievedHistory2 = cache.get(makefile, repos, true, Boolean.TRUE); assertTrue("entry set should be empty", retrievedHistory2.getHistoryEntries().isEmpty()); List entries = retrievedHistory.getHistoryEntries(); assertEquals("Unexpected number of entries", 2, entries.size()); final String TROND = "Trond Norbye "; Iterator entryIt = entries.iterator(); HistoryEntry e1 = entryIt.next(); assertEquals(TROND, e1.getAuthor()); assertEquals("2:585a1b3f2efb", e1.getRevision()); assertEquals(2, e1.getFiles().size()); HistoryEntry e2 = entryIt.next(); assertEquals(TROND, e2.getAuthor()); assertEquals("1:f24a5fd7a85d", e2.getRevision()); assertEquals(3, e2.getFiles().size()); assertFalse(entryIt.hasNext()); // test get history for directory History dirHistory = cache.get(reposRoot, repos, true, null); assertSameEntries(historyToStore.getHistoryEntries(), dirHistory.getHistoryEntries()); retrievedHistory2 = cache.get(reposRoot, repos, true, Boolean.TRUE); assertSameEntries(historyToStore.getHistoryEntries(), retrievedHistory2.getHistoryEntries()); retrievedHistory2 = cache.get(reposRoot, repos, true, Boolean.FALSE); assertTrue("entry set should be empty" , retrievedHistory2.getHistoryEntries().isEmpty()); // test incremental update importHgChangeset( reposRoot, getClass().getResource("hg-export.txt").getPath()); repos.createCache(cache, cache.getLatestCachedRevision(repos)); cache.optimize(); History updatedHistory = cache.get(reposRoot, repos, true, null); long time = 1245446973L; // whole seconds only if (RuntimeEnvironment.isOldJVM()) { time /= 60; // whole minutes only time *= 60; } HistoryEntry newEntry = new HistoryEntry( "3:78649c3ec6cb", null, new Date(time * 1000), "xyz", "Return failure when executed with no arguments", true); newEntry.addFile("/mercurial/main.c"); LinkedList updatedEntries = new LinkedList( updatedHistory.getHistoryEntries()); assertSameEntry(newEntry, updatedEntries.removeFirst()); assertSameEntries(historyToStore.getHistoryEntries(), updatedEntries); // prepare test for non-existing, i.e. removed file/directory using // isDir = {null, true, false} newEntry.addFile("/mercurial/subdir/bla.c"); newEntry.setRevision("4:78649c3ec6cb"); updatedEntries.clear(); updatedEntries.add(newEntry); dirHistory.setHistoryEntries(updatedEntries); //dumpDB("0pre"); cache.store(dirHistory, repos); //dumpDB("0post"); File dir = new File(repos.getDirectoryName(), "subdir"); assertTrue(cache.hasCacheForDirectory(dir, repos)); File file = new File(dir, "bla.c"); assertFalse(cache.hasCacheForDirectory(file, repos)); // not a dir // null == auto -> test for physical path -> fails -> assumes path is file assertTrue(cache.get(dir, repos, false, null).getHistoryEntries().isEmpty()); assertFalse(cache.get(file, repos, false, null).getHistoryEntries().isEmpty()); // force to take as file (same results as for null) assertTrue(cache.get(dir, repos, false, false).getHistoryEntries().isEmpty()); assertFalse(cache.get(file, repos, false, false).getHistoryEntries().isEmpty()); // force to take as dir assertFalse(cache.get(dir, repos, false, true).getHistoryEntries().isEmpty()); assertTrue(cache.get(file, repos, false, true).getHistoryEntries().isEmpty()); // test clearing of cache cache.clear(repos); History clearedHistory = cache.get(reposRoot, repos, true, null); assertTrue("History should be empty", clearedHistory.getHistoryEntries().isEmpty()); cache.store(historyToStore, repos); assertSameEntries(historyToStore.getHistoryEntries(), cache.get(reposRoot, repos, true, null).getHistoryEntries()); } /** * Test that {@code getLatestCachedRevision()} returns the correct * revision. * @throws Exception */ public void testGetLatestCachedRevision() throws Exception { File reposRoot = new File(repositories.getSourceRoot(), "mercurial"); Repository repos = RepositoryFactory.getRepository(reposRoot); History history = repos.getHistory(reposRoot); cache.store(history, repos); cache.optimize(); List entries = history.getHistoryEntries(); HistoryEntry oldestEntry = entries.get(entries.size() - 1); HistoryEntry mostRecentEntry = entries.get(0); assertTrue("Unexpected order of history entries", oldestEntry.getDate().before(mostRecentEntry.getDate())); String latestRevision = mostRecentEntry.getRevision(); assertNotNull("Unknown latest revision", latestRevision); assertEquals("Incorrect latest revision", latestRevision, cache.getLatestCachedRevision(repos)); // test incremental update importHgChangeset( reposRoot, getClass().getResource("hg-export.txt").getPath()); repos.createCache(cache, latestRevision); assertEquals("3:78649c3ec6cb", cache.getLatestCachedRevision(repos)); } /** * Test that {@code hasCacheForDirectory()} works. * @throws Exception */ public void testHasCacheForDirectory() throws Exception { // Use a Mercurial repository and a Subversion repository in this test. File hgRoot = new File(repositories.getSourceRoot(), "mercurial"); Repository hgRepos = RepositoryFactory.getRepository(hgRoot); File svnRoot = new File(repositories.getSourceRoot(), "svn"); Repository svnRepos = RepositoryFactory.getRepository(svnRoot); // None of the repositories should have any history. assertFalse(cache.hasCacheForDirectory(hgRoot, hgRepos)); assertFalse(cache.hasCacheForDirectory(svnRoot, svnRepos)); // Store empty history, so still expect false. cache.store(new History(), hgRepos); cache.store(new History(), svnRepos); assertFalse(cache.hasCacheForDirectory(hgRoot, hgRepos)); assertFalse(cache.hasCacheForDirectory(svnRoot, svnRepos)); // Store history for Mercurial repository. cache.store(hgRepos.getHistory(hgRoot), hgRepos); assertTrue(cache.hasCacheForDirectory(hgRoot, hgRepos)); assertFalse(cache.hasCacheForDirectory(svnRoot, svnRepos)); // Store history for Subversion repository. //dumpDB("pre"); cache.store(hgRepos.getHistory(svnRoot), svnRepos); //dumpDB("post"); assertTrue(cache.hasCacheForDirectory(hgRoot, hgRepos)); // w/o mercurial repo fix there is a /mercurial subdir, but no /svn subdir // w/ mercurial repo fix there is still no subdir assertFalse(cache.hasCacheForDirectory(svnRoot, svnRepos)); } /** * Test that get() is able to continue and return successfully after a lock * timeout when accessing the database. * @throws Exception */ public void testRetryGetOnTimeout() throws Exception { File reposRoot = new File(repositories.getSourceRoot(), "mercurial"); Repository repos = RepositoryFactory.getRepository(reposRoot); History history = repos.getHistory(reposRoot); cache.store(history, repos); // Set the lock timeout to one second to make it go faster. final Connection c = DriverManager.getConnection(getURL()); Statement s = c.createStatement(); s.execute("call syscs_util.syscs_set_database_property" + "('derby.locks.waitTimeout', '1')"); // Lock one of the tables exclusively in order to block get(). c.setAutoCommit(false); // Originally, we locked the FILECHANGES table here, but that triggered // a Derby bug (https://issues.apache.org/jira/browse/DERBY-4330), so // now we lock the AUTHORS table instead. s.execute("lock table opengrok.authors in exclusive mode"); s.close(); // Roll back the transaction in 1.5 seconds so that get() is able to // continue after the first timeout. final Exception[] ex = new Exception[1]; Thread t = new Thread() { @Override public void run() { try { Thread.sleep(1500); c.rollback(); c.close(); } catch (Exception e) { ex[0] = e; } } }; t.start(); // get() should be able to continue after a timeout. assertSameEntries( history.getHistoryEntries(), cache.get(reposRoot, repos, true, null).getHistoryEntries()); t.join(); // Expose any exception thrown in the helper thread. if (ex[0] != null) { throw ex[0]; } } /** * Regression test for bug #11663. If the commit message was longer than * the maximum VARCHAR length, a truncation error would be raised by the * database. Now the message should be truncated if such a message is * encountered. * @throws Exception */ public void testVeryLongCommitMessage() throws Exception { File reposRoot = new File(repositories.getSourceRoot(), "mercurial"); Repository r = RepositoryFactory.getRepository(reposRoot); HistoryEntry e0 = new HistoryEntry(); e0.setAuthor("dummy"); e0.setDate(new Date()); e0.addFile("/mercurial/readme.txt"); e0.setRevision("12:abcdef123456"); for (int msgLength = 0; msgLength < 40000; msgLength += 48) { e0.appendMessage( "this is a line with 48 chars, including newline"); } // Used to get a truncation error from the database here. cache.store(new History(Collections.singletonList(e0)), r); History h = cache.get(reposRoot, r, false, null); assertEquals("One entry expected", 1, h.getHistoryEntries().size()); HistoryEntry e1 = h.getHistoryEntries().get(0); assertEquals("Author", e0.getAuthor(), e1.getAuthor()); assertEquals("Date", e0.getDate(), e1.getDate()); assertEquals("Revision", e0.getRevision(), e1.getRevision()); assertTrue("No file list requested", e1.getFiles().isEmpty()); assertFalse("Long message should be truncated", e0.getMessage().equals(e1.getMessage())); assertEquals("Start of message should be equal to original", e0.getMessage().substring(0, 1000), e1.getMessage().substring(0, 1000)); } @SuppressWarnings("javadoc") public void testGetInfo() { String info = cache.getInfo(); assertTrue("Info should contain name of history cache", info.startsWith("JDBCHistoryCache")); assertTrue("Info should contain driver class", info.contains(DERBY_EMBEDDED_DRIVER)); } /** * Test that it is possible to store an entry with no author. * Bug #11662. * @throws Exception */ public void testNullAuthor() throws Exception { File reposRoot = new File(repositories.getSourceRoot(), "svn"); Repository r = RepositoryFactory.getRepository(reposRoot); // Create an entry where author is null HistoryEntry e = new HistoryEntry( "1", null, new Date(), null, "Initial revision", true); e.addFile("/svn/file.txt"); List entries = Collections.singletonList(e); cache.store(new History(entries), r); assertSameEntries( entries, cache.get(reposRoot, r, true, null).getHistoryEntries()); } }