diff --git a/core/src/main/java/hudson/model/FingerprintCleanupThread.java b/core/src/main/java/hudson/model/FingerprintCleanupThread.java index 59187a3a400630e63968c886541652f8c439e7b4..292248ed528336439dc96c1cef7306b9881cd6ff 100644 --- a/core/src/main/java/hudson/model/FingerprintCleanupThread.java +++ b/core/src/main/java/hudson/model/FingerprintCleanupThread.java @@ -28,9 +28,10 @@ import hudson.ExtensionList; import hudson.Functions; import jenkins.model.Jenkins; import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import java.io.File; -import java.io.FileFilter; import java.io.IOException; import java.util.regex.Pattern; @@ -45,7 +46,11 @@ import java.util.regex.Pattern; * @author Kohsuke Kawaguchi */ @Extension @Symbol("fingerprintCleanup") -public final class FingerprintCleanupThread extends AsyncPeriodicWork { +@Restricted(NoExternalUse.class) +public class FingerprintCleanupThread extends AsyncPeriodicWork { + + static final String FINGERPRINTS_DIR_NAME = "fingerprints"; + private static final Pattern FINGERPRINT_FILE_PATTERN = Pattern.compile("[0-9a-f]{28}\\.xml"); public FingerprintCleanupThread() { super("Fingerprint cleanup"); @@ -66,13 +71,13 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork { public void execute(TaskListener listener) { int numFiles = 0; - File root = new File(Jenkins.getInstance().getRootDir(),"fingerprints"); - File[] files1 = root.listFiles(LENGTH2DIR_FILTER); + File root = new File(getRootDir(), FINGERPRINTS_DIR_NAME); + File[] files1 = root.listFiles(f -> f.isDirectory() && f.getName().length()==2); if(files1!=null) { for (File file1 : files1) { - File[] files2 = file1.listFiles(LENGTH2DIR_FILTER); + File[] files2 = file1.listFiles(f -> f.isDirectory() && f.getName().length()==2); for(File file2 : files2) { - File[] files3 = file2.listFiles(FINGERPRINTFILE_FILTER); + File[] files3 = file2.listFiles(f -> f.isFile() && FINGERPRINT_FILE_PATTERN.matcher(f.getName()).matches()); for(File file3 : files3) { if(check(file3, listener)) numFiles++; @@ -101,7 +106,7 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork { */ private boolean check(File fingerprintFile, TaskListener listener) { try { - Fingerprint fp = Fingerprint.load(fingerprintFile); + Fingerprint fp = loadFingerprint(fingerprintFile); if (fp == null || !fp.isAlive()) { listener.getLogger().println("deleting obsolete " + fingerprintFile); fingerprintFile.delete(); @@ -109,8 +114,7 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork { } else { // get the fingerprint in the official map so have the changes visible to Jenkins // otherwise the mutation made in FingerprintMap can override our trimming. - listener.getLogger().println("possibly trimming " + fingerprintFile); - fp = Jenkins.getInstance()._getFingerprint(fp.getHashString()); + fp = getFingerprint(fp); return fp.trim(); } } catch (IOException e) { @@ -119,17 +123,16 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork { } } - private static final FileFilter LENGTH2DIR_FILTER = new FileFilter() { - public boolean accept(File f) { - return f.isDirectory() && f.getName().length()==2; - } - }; + protected Fingerprint loadFingerprint(File fingerprintFile) throws IOException { + return Fingerprint.load(fingerprintFile); + } - private static final FileFilter FINGERPRINTFILE_FILTER = new FileFilter() { - private final Pattern PATTERN = Pattern.compile("[0-9a-f]{28}\\.xml"); + protected Fingerprint getFingerprint(Fingerprint fp) throws IOException { + return Jenkins.get()._getFingerprint(fp.getHashString()); + } + + protected File getRootDir() { + return Jenkins.get().getRootDir(); + } - public boolean accept(File f) { - return f.isFile() && PATTERN.matcher(f.getName()).matches(); - } - }; } diff --git a/core/src/test/java/hudson/model/FingerprintCleanupThreadTest.java b/core/src/test/java/hudson/model/FingerprintCleanupThreadTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3fd255535a1a089f9494f3897cb065b677727e43 --- /dev/null +++ b/core/src/test/java/hudson/model/FingerprintCleanupThreadTest.java @@ -0,0 +1,180 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.model; + +import org.junit.Test; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class FingerprintCleanupThreadTest { + + private static final Fingerprint.BuildPtr ptr = new Fingerprint.BuildPtr("fred", 23); + private Path tempDirectory; + private Path fpFile; + + @Test + public void testDoesNotLogUnimportantExcessiveLogMessage() throws IOException { + createFolderStructure(); + TestTaskListener testTaskListener = new TestTaskListener(); + FingerprintCleanupThread cleanupThread = new TestFingerprintCleanupThread(new TestFingerprint(true)); + cleanupThread.execute(testTaskListener); + String logOutput = testTaskListener.outputStream.toString(); + assertFalse("Should not have logged unimportant, excessive message.", logOutput.contains("possibly trimming")); + } + + @Test + public void testFingerprintFileIsEmpty() throws IOException { + createFolderStructure(); + TestTaskListener testTaskListener = new TestTaskListener(); + FingerprintCleanupThread cleanupThread = new TestFingerprintCleanupThread(new TestFingerprint(false)); + cleanupThread.execute(testTaskListener); + String logOutput = testTaskListener.outputStream.toString(); + assertFalse("Should have deleted obsolete file.", fpFile.toFile().exists()); + } + + @Test + public void testGetRecurencePeriod() throws IOException { + FingerprintCleanupThread cleanupThread = new TestFingerprintCleanupThread(new TestFingerprint()); + assertEquals("Wrong recurrence period.", PeriodicWork.DAY, cleanupThread.getRecurrencePeriod()); + } + + @Test + public void testNoFingerprintsDir() throws IOException { + createTestDir(); + TestTaskListener testTaskListener = new TestTaskListener(); + FingerprintCleanupThread cleanupThread = new TestFingerprintCleanupThread(new TestFingerprint()); + cleanupThread.execute(testTaskListener); + String logOutput = testTaskListener.outputStream.toString(); + assertTrue("Should have done nothing.", logOutput.startsWith("Cleaned up 0 records")); + } + + @Test + public void testIOExceptionOnLoad() throws IOException { + createFolderStructure(); + TestTaskListener testTaskListener = new TestTaskListener(); + FingerprintCleanupThread cleanupThread = new TestFingerprintCleanupThreadThrowsExceptionOnLoad(new TestFingerprint()); + cleanupThread.execute(testTaskListener); + String logOutput = testTaskListener.outputStream.toString(); + assertTrue("Should have logged IOException.", logOutput.contains("ERROR: Failed to process")); + } + + private void createFolderStructure() throws IOException { + createTestDir(); + Path fingerprintsPath = tempDirectory.resolve(FingerprintCleanupThread.FINGERPRINTS_DIR_NAME); + Files.createDirectory(fingerprintsPath); + Path aaDir = fingerprintsPath.resolve("aa"); + Files.createDirectory(aaDir); + Path bbDir = aaDir.resolve("bb"); + Files.createDirectory(bbDir); + fpFile = bbDir.resolve("0123456789012345678901234567.xml"); + Files.createFile(fpFile); + } + + private void createTestDir() throws IOException { + tempDirectory = Files.createTempDirectory(Paths.get("target"), "fpCleanupThreadTest"); + tempDirectory.toFile().deleteOnExit(); + } + + private class TestTaskListener implements TaskListener { + + private ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + private PrintStream logStream = new PrintStream(outputStream); + + @Nonnull + @Override + public PrintStream getLogger() { + return logStream; + } + + } + + private class TestFingerprintCleanupThread extends FingerprintCleanupThread { + + private Fingerprint fingerprintToLoad; + + public TestFingerprintCleanupThread(Fingerprint fingerprintToLoad) throws IOException { + this.fingerprintToLoad = fingerprintToLoad; + return; + } + + @Override + protected Fingerprint getFingerprint(Fingerprint fp) throws IOException { + return new Fingerprint(ptr, "file", new byte[0]); + } + + @Override + protected File getRootDir() { + return tempDirectory.toFile(); + } + + @Override + protected Fingerprint loadFingerprint(File fingerprintFile) throws IOException { + return fingerprintToLoad; + } + + } + + private class TestFingerprint extends Fingerprint { + + private boolean isAlive = true; + + public TestFingerprint() throws IOException { + super(ptr, "fred", new byte[0]); + } + + public TestFingerprint(boolean isAlive) throws IOException { + super(ptr, "fred", new byte[0]); + this.isAlive = isAlive; + } + + @Override + public synchronized boolean isAlive() { + return isAlive; + } + } + + private class TestFingerprintCleanupThreadThrowsExceptionOnLoad extends TestFingerprintCleanupThread { + + public TestFingerprintCleanupThreadThrowsExceptionOnLoad(Fingerprint fingerprintToLoad) throws IOException { + super(fingerprintToLoad); + } + + @Override + protected Fingerprint loadFingerprint(File fingerprintFile) throws IOException { + throw new IOException("Test exception"); + } + } +}