/* * 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 (c) 2009, 2012, Oracle and/or its affiliates. All rights reserved. */ package org.opensolaris.opengrok.history; import java.io.File; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; 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.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 final String DERBY_EMBEDDED_DRIVER = "org.apache.derby.jdbc.EmbeddedDriver"; 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)"); } } /** * 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 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 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 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. */ 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); 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); assertSameEntries( historyToStore.getHistoryEntries(), dirHistory.getHistoryEntries()); // 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); HistoryEntry newEntry = new HistoryEntry( "3:78649c3ec6cb", new Date(1245446973L / 60 * 60 * 1000), // whole minutes only "xyz", null, "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); // test clearing of cache cache.clear(repos); History clearedHistory = cache.get(reposRoot, repos, true); assertTrue("History should be empty", clearedHistory.getHistoryEntries().isEmpty()); cache.store(historyToStore, repos); assertSameEntries(historyToStore.getHistoryEntries(), cache.get(reposRoot, repos, true).getHistoryEntries()); } /** * Test that {@code getLatestCachedRevision()} returns the correct * revision. */ 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. */ 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. cache.store(svnRepos.getHistory(svnRoot), svnRepos); assertTrue(cache.hasCacheForDirectory(hgRoot, hgRepos)); assertTrue(cache.hasCacheForDirectory(svnRoot, svnRepos)); // Bug #19230: Test that it works for sub-directories too, not only // for the repository root. assertTrue( cache.hasCacheForDirectory(new File(svnRoot, "c"), svnRepos)); assertTrue( cache.hasCacheForDirectory(new File(svnRoot, "lisp"), svnRepos)); // Also test that we don't claim to have history for a directory // that's not under version control, even if it lives in a version // controlled source tree. File notCheckedIn = new File(svnRoot, "not-checked-in"); assertTrue(notCheckedIn.mkdir()); assertFalse(cache.hasCacheForDirectory(notCheckedIn, svnRepos)); } /** * Test that get() is able to continue and return successfully after a lock * timeout when accessing the database. */ 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).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. */ 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); 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)); } public void testGetInfo() throws HistoryException { 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. */ 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", new Date(), null, 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).getHistoryEntries()); } }