package hudson.plugins.jobConfigHistory; import hudson.XmlFile; import hudson.model.FreeStyleProject; import hudson.security.HudsonPrivateSecurityRealm; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import com.gargoylesoftware.htmlunit.html.HtmlForm; /** * @author jborghi@cisco.com * */ public class JobConfigHistoryTest extends AbstractHudsonTestCaseDeletingInstanceDir { private WebClient webClient; // we need to sleep between saves so we don't overwrite the history directories // (which are saved with a granularity of one second) private static final int SLEEP_TIME = 1100; private static final FileFilter DELETE_FILTER = new FileFilter() { public boolean accept(File file) { if (file.isDirectory()) { file.listFiles(this); } file.delete(); return false; } }; @Override protected void setUp() throws Exception { super.setUp(); hudson.setSecurityRealm(new HudsonPrivateSecurityRealm(true)); webClient = new WebClient(); } public void testJobConfigHistoryPreConfigured() { final JobConfigHistory jch = hudson.getPlugin(JobConfigHistory.class); try { HtmlForm form = webClient.goTo("configure").getFormByName("config"); form.getInputByName("maxHistoryEntries").setValueAttribute("10"); form.getInputByName("saveSystemConfiguration").setChecked(true); form.getInputByName("skipDuplicateHistory").setChecked(false); form.getInputByName("excludePattern").setValueAttribute(JobConfigHistoryConsts.DEFAULT_EXCLUDE); form.getInputByName("historyRootDir").setValueAttribute("jobConfigHistory"); submit(form); } catch (Exception e) { fail("unable to configure Hudson instance " + e); } assertEquals("Verify history entries to keep setting.", "10", jch.getMaxHistoryEntries()); assertTrue("Verify system level configurations setting.", jch.getSaveSystemConfiguration()); assertFalse("Verify skip duplicate history setting.", jch.getSkipDuplicateHistory()); assertEquals("Verify configured history root directory.",new File(hudson.root, "jobConfigHistory"), jch.getConfiguredHistoryRootDir()); assertEquals("Verify exclude pattern setting.", JobConfigHistoryConsts.DEFAULT_EXCLUDE, jch.getExcludePattern()); XmlFile hudsonConfig = new XmlFile(new File(hudson.getRootDir(),"config.xml")); assertTrue("Verify a system level configuration is saveable.", jch.isSaveable(hudson, hudsonConfig)); assertTrue("Verify system configuration history location", jch.getHistoryDir(hudsonConfig).getParentFile().equals(jch.getSystemHistoryDir())); testCreateRenameDeleteProject(jch); assertNull("Verify null when attempting to get history dir for a file outside of HUDSON_ROOT.", jch.getHistoryDir(new XmlFile(new File("/tmp")))); assertFalse("Verify false when testing if a file outside of HUDSON_ROOT is saveable.", jch.isSaveable(null, new XmlFile(new File("/tmp/config.xml")))); } public void testJobConfigHistoryDefaults() { final JobConfigHistory jch = hudson.getPlugin(JobConfigHistory.class); assertNull("Verify number of history entries to keep default setting.", jch.getMaxHistoryEntries()); assertFalse("Verify system level configurations default setting.", jch.getSaveSystemConfiguration()); assertFalse("Verify skip duplicate history default setting.", jch.getSkipDuplicateHistory()); assertNull("Verify history root directory default.", jch.getConfiguredHistoryRootDir()); assertNull("Verify unconfigured exclude pattern.", jch.getExcludePattern()); XmlFile hudsonConfig = new XmlFile(new File(hudson.getRootDir(), "config.xml")); assertFalse("Verify a system level configuration is not saveable.", jch.isSaveable(hudson, hudsonConfig)); assertTrue("Verify system configuration history location", jch.getHistoryDir(hudsonConfig).getParentFile().equals(jch.getSystemHistoryDir())); testCreateRenameDeleteProject(jch); } public void testSkipDuplicateHistory() { final JobConfigHistory jch = hudson.getPlugin(JobConfigHistory.class); try { HtmlForm form = webClient.goTo("configure").getFormByName("config"); form.getInputByName("saveSystemConfiguration").setChecked(true); form.getInputByName("skipDuplicateHistory").setChecked(true); submit(form); final FreeStyleProject project = createFreeStyleProject("testproject"); final File projectHistoryDir = jch.getHistoryDir(project.getConfigFile()); final JobConfigHistoryProjectAction projectAction = new JobConfigHistoryProjectAction(project); // clear out all history - setting to 1 will clear out all with the expectation that we are creating a new entry jch.setMaxHistoryEntries("1"); jch.checkForPurgeByQuantity(projectHistoryDir); // reset to empty value jch.setMaxHistoryEntries(""); for (int i = 0 ; i < 3 ; i++) { Thread.sleep(SLEEP_TIME); project.save(); } assertEquals("Verify 1 project history entry after 3 duplicate saves.", 1, projectAction.getConfigs().size()); // system history test - skip duplicate history -hardcode path to Hudson config final File hudsonConfigDir = new File(hudson.root, JobConfigHistoryConsts.DEFAULT_HISTORY_DIR + "/config"); for (int i = 0 ; i < 3 ; i++) { Thread.sleep(SLEEP_TIME); hudson.save(); } assertEquals("Verify 1 system history entry after 3 duplicate saves.", 1, hudsonConfigDir.listFiles(JobConfigHistory.HISTORY_FILTER).length); // verify non-duplicate history is saved project.setDescription("new description"); project.save(); assertEquals("Verify non duplicate project history saved.", 2, projectAction.getConfigs().size()); // corrupt history record and verify new entry will be saved final File[] historyDirs = jch.getHistoryDir(project.getConfigFile()).listFiles(JobConfigHistory.HISTORY_FILTER); Arrays.sort(historyDirs, Collections.reverseOrder()); (new File(historyDirs[0], "config.xml")).renameTo(new File(historyDirs[0], "config")); assertNull("Verify history dir is corrupted.", jch.getConfigFile(historyDirs[0])); assertTrue("Verify configuration is saveable when history is corrupted.", jch.isSaveable(project, project.getConfigFile())); // reconfigure to allow saving duplicate history form = webClient.goTo("configure").getFormByName("config"); form.getInputByName("skipDuplicateHistory").setChecked(false); submit(form); // perform additional save and verify more than one history entries exist Thread.sleep(SLEEP_TIME); hudson.save(); project.save(); assertTrue("Verify duplicate project history entries.", projectAction.getConfigs().size() > 2); assertTrue("Verify duplicate system history entries.", hudsonConfigDir.listFiles(JobConfigHistory.HISTORY_FILTER).length > 1); } catch (Exception e) { fail("Unable to complete duplicate history test: " + e); } } public void testFormValidation() { final JobConfigHistory jch = hudson.getPlugin(JobConfigHistory.class); try { final HtmlForm form = webClient.goTo("configure").getFormByName("config"); assertFalse("Check no error message present for history entry.", form.getTextContent().contains("Enter a valid positive integer")); form.getInputByName("maxHistoryEntries").setValueAttribute("-2"); assertTrue("Check error message on invalid history entry.", form.getTextContent().contains("Enter a valid positive integer")); assertFalse("Check no error messgae present for regexp excludePattern.",form.getTextContent().contains("Invalid regexp")); form.getInputByName("excludePattern").setValueAttribute("**"); assertTrue("Check error message on invalid regexp excludePattern.",form.getTextContent().contains("Invalid regexp")); submit(form); assertEquals("Verify invalid regexp string is saved.", "**", jch.getExcludePattern()); assertNull("Verify invalid regexp has not been loaded.", jch.getExcludeRegexpPattern()); } catch (Exception e) { fail("Unable to complete form validation: " + e); } } public void testMaxHistoryEntries() { try { final HtmlForm form = webClient.goTo("configure").getFormByName("config"); form.getInputByName("maxHistoryEntries").setValueAttribute("5"); form.getInputByName("skipDuplicateHistory").setChecked(false); submit(form); final FreeStyleProject project = createFreeStyleProject("testproject"); final JobConfigHistoryProjectAction projectAction = new JobConfigHistoryProjectAction(project); assertEquals("Verify 1 history entry created.", 1, projectAction.getConfigs().size()); for (int i = 0 ; i < 3 ; i++) { Thread.sleep(SLEEP_TIME); project.save(); } assertEquals("Verify 4 history entries.", 4, projectAction.getConfigs().size()); for (int i = 0 ; i < 3 ; i++) { Thread.sleep(SLEEP_TIME); project.save(); } assertEquals("Verify no more than 5 history entries created.", 5, projectAction.getConfigs().size()); } catch (Exception e) { fail("Unable to complete max history entries test: " + e); } } public void testPurgeByQuantity() { final JobConfigHistory jch = hudson.getPlugin(JobConfigHistory.class); try { final FreeStyleProject project = createFreeStyleProject("newproject"); final File historyDir = jch.getHistoryDir(project.getConfigFile()); final JobConfigHistoryProjectAction projectAction = new JobConfigHistoryProjectAction(project); // check with default value jch.checkForPurgeByQuantity(historyDir); assertEquals("Verify 1 history entry exists, default purge quantity.", 1, projectAction.getConfigs().size()); // set to negative value, ensure no purge happens jch.setMaxHistoryEntries("-1"); jch.checkForPurgeByQuantity(historyDir); assertEquals("Verify 1 history entry, invalid max quantity.", 1, projectAction.getConfigs().size()); // set to 2, ensure no purge happens jch.setMaxHistoryEntries("2"); assertEquals("Verify 1 history entry, max entries > current.", 1, projectAction.getConfigs().size()); // purge attempt on invalid directory jch.checkForPurgeByQuantity(new File("/invaliddir")); assertEquals("Verify history unaffected (still 1 entry) after attempt to purge invalid directory.", 1, projectAction.getConfigs().size()); // clear out all history - setting to 1 will clear out all with the expectation that we are creating a new entry jch.setMaxHistoryEntries("1"); jch.checkForPurgeByQuantity(historyDir); assertEquals("Verify no history entries remain.", 0, projectAction.getConfigs().size()); // recreate a history entry, set to read-only status, verify it is not deleted // NOTE: Windows host seem to ignore the setWritable flag, so the following test will fail on Windows. // A somewhat crude verification for a windows host. if ((new File("c:/")).exists()) { System.out.println("Skipping permission based rename tests - Windows system detected."); } else { final ArrayList<File> toRestore = new ArrayList<File>(); // to allow cleanup // create a new history entry project.save(); for(final File file : historyDir.listFiles()) { file.setWritable(false); toRestore.add(file); } historyDir.setWritable(false); toRestore.add(historyDir); jch.checkForPurgeByQuantity(historyDir); assertEquals("Verify purge did not happen.", 1, projectAction.getConfigs().size()); for(final File file : toRestore) { file.setWritable(true); } } } catch (IOException e) { fail("Unable to complete purge test: " + e); } } public void testAbsPathHistoryRootDir() { final JobConfigHistory jch = hudson.getPlugin(JobConfigHistory.class); try { // create a unique name, then delete the empty file - will be recreated later final File root = File.createTempFile("jobConfigHistory.test_abs_path", null); final String absolutePath = root.getPath(); root.delete(); final HtmlForm form = webClient.goTo("configure").getFormByName("config"); form.getInputByName("historyRootDir").setValueAttribute(absolutePath); submit(form); assertEquals("Verify history root configured at absolute path.", root, jch.getConfiguredHistoryRootDir()); // save something createFreeStyleProject(); assertTrue("Verify history root exists.", root.exists()); // cleanup - Hudson doesn't know about these files we created root.listFiles(DELETE_FILTER); root.delete(); // not really needed, but helpful so we don't clutter the test host with unnecessary files assertFalse("Verify cleanup of history files: " + root, root.exists()); } catch (Exception e) { fail("Unable to complete history root absolute path test: " + e); } } private void testCreateRenameDeleteProject(final JobConfigHistory jch) { try { final FreeStyleProject project = createFreeStyleProject("testproject"); final File configuredHistoryRootFile = jch.getConfiguredHistoryRootDir(); // project configs should always be saveable assertTrue("Verify new project is a saveable item.", jch.isSaveable(project, project.getConfigFile())); // by default, the expected config dir is in a 'config-history' directory under the project // otherwise it is located under the configured root directory final File expectedConfigDir; if (jch.getConfiguredHistoryRootDir() == null) { expectedConfigDir = new File(hudson.root, "jobs/testproject/" + JobConfigHistoryConsts.DEFAULT_HISTORY_DIR); } else { expectedConfigDir = new File(configuredHistoryRootFile, "jobs/testproject"); } assertEquals("Verify history dir configured as expected.", expectedConfigDir, jch.getHistoryDir(project.getConfigFile())); assertTrue("Verify project config history directory created: " + expectedConfigDir, expectedConfigDir.exists()); assertEquals("Verify one history entry on creation.", 1, expectedConfigDir.listFiles(JobConfigHistory.HISTORY_FILTER).length); // sleep so we don't overwrite our existing history directory Thread.sleep(SLEEP_TIME); project.renameTo("renamed_testproject"); // verify rename moves the history directory as expected assertFalse("Verify on rename old project config history directory removed.", expectedConfigDir.exists()); final File newExpectedConfigDir = new File(expectedConfigDir.toString().replace("testproject","renamed_testproject")); assertTrue("Verify renamed project config history created: " + newExpectedConfigDir, newExpectedConfigDir.exists()); assertEquals("Verify two history entries after rename.", 2, newExpectedConfigDir.listFiles(JobConfigHistory.HISTORY_FILTER).length); // delete project and verify the history directory is gone project.delete(); assertFalse("Verify on delete project config history directory removed(renamed): " + newExpectedConfigDir, newExpectedConfigDir.exists()); } catch (IOException e) { fail("Unable to complete project creation/rename test: " + e); } catch (InterruptedException e) { fail("Interrupted, unable to test project deletion: " + e); } } }