UtilTest.java 27.1 KB
Newer Older
K
kohsuke 已提交
1 2
/*
 * The MIT License
3
 *
4 5
 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
 * Daniel Dyer, Erik Ramfelt, Richard Bair, id:cactusman
6
 *
K
kohsuke 已提交
7 8 9 10 11 12
 * 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:
13
 *
K
kohsuke 已提交
14 15
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
16
 *
K
kohsuke 已提交
17 18 19 20 21 22 23 24
 * 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.
 */
K
kohsuke 已提交
25 26
package hudson;

27
import java.util.List;
K
kohsuke 已提交
28 29
import java.util.Map;
import java.util.HashMap;
30
import java.util.Locale;
31
import java.util.Properties;
32
import java.util.concurrent.Callable;
33
import java.util.concurrent.atomic.AtomicReference;
34
import java.io.ByteArrayOutputStream;
K
kohsuke 已提交
35
import java.io.File;
36
import java.io.FileInputStream;
37
import java.io.IOException;
38
import java.io.InputStream;
K
kohsuke 已提交
39

40
import static org.hamcrest.CoreMatchers.allOf;
41 42
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.hasItem;
43
import static org.hamcrest.CoreMatchers.instanceOf;
44
import static org.hamcrest.CoreMatchers.not;
45
import static org.junit.Assert.*;
46

47
import org.apache.commons.io.FileUtils;
48 49
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
50
import org.junit.Assume;
51
import org.junit.Test;
52
import org.jvnet.hudson.test.Issue;
53

K
kohsuke 已提交
54
import hudson.util.StreamTaskListener;
55

56
import org.junit.Rule;
57
import org.junit.internal.AssumptionViolatedException;
58
import org.junit.rules.TemporaryFolder;
K
kohsuke 已提交
59

60 61
import com.google.common.collect.Lists;

K
kohsuke 已提交
62 63 64
/**
 * @author Kohsuke Kawaguchi
 */
65
public class UtilTest {
66 67 68

    @Rule public TemporaryFolder tmp = new TemporaryFolder();

69
    @Test
K
kohsuke 已提交
70 71 72
    public void testReplaceMacro() {
        Map<String,String> m = new HashMap<String,String>();
        m.put("A","a");
73
        m.put("A.B","a-b");
K
kohsuke 已提交
74 75
        m.put("AA","aa");
        m.put("B","B");
76 77
        m.put("DOLLAR", "$");
        m.put("ENCLOSED", "a${A}");
K
kohsuke 已提交
78 79 80 81 82 83

        // longest match
        assertEquals("aa",Util.replaceMacro("$AA",m));

        // invalid keys are ignored
        assertEquals("$AAB",Util.replaceMacro("$AAB",m));
84

85 86 87
        assertEquals("aaB",Util.replaceMacro("${AA}B",m));
        assertEquals("${AAB}",Util.replaceMacro("${AAB}",m));

88 89
        // $ escaping
        assertEquals("asd$${AA}dd", Util.replaceMacro("asd$$$${AA}dd",m));
K
kohsuke 已提交
90 91
        assertEquals("$", Util.replaceMacro("$$",m));
        assertEquals("$$", Util.replaceMacro("$$$$",m));
92

93 94 95
        // dots
        assertEquals("a.B", Util.replaceMacro("$A.B", m));
        assertEquals("a-b", Util.replaceMacro("${A.B}", m));
96

97
    	// test that more complex scenarios work
98
        assertEquals("/a/B/aa", Util.replaceMacro("/$A/$B/$AA",m));
99 100
        assertEquals("a-aa", Util.replaceMacro("$A-$AA",m));
        assertEquals("/a/foo/can/B/you-believe_aa~it?", Util.replaceMacro("/$A/foo/can/$B/you-believe_$AA~it?",m));
101
        assertEquals("$$aa$Ba${A}$it", Util.replaceMacro("$$$DOLLAR${AA}$$B${ENCLOSED}$it",m));
K
kohsuke 已提交
102
    }
103

104
    @Test
105 106 107 108 109 110
    public void testTimeSpanString() {
        // Check that amounts less than 365 days are not rounded up to a whole year.
        // In the previous implementation there were 360 days in a year.
        // We're still working on the assumption that a month is 30 days, so there will
        // be 5 days at the end of the year that will be "12 months" but not "1 year".
        // First check 359 days.
C
cactusman 已提交
111
        assertEquals(Messages.Util_month(11), Util.getTimeSpanString(31017600000L));
112
        // And 362 days.
C
cactusman 已提交
113
        assertEquals(Messages.Util_month(12), Util.getTimeSpanString(31276800000L));
114 115

        // 11.25 years - Check that if the first unit has 2 or more digits, a second unit isn't used.
C
cactusman 已提交
116
        assertEquals(Messages.Util_year(11), Util.getTimeSpanString(354780000000L));
117
        // 9.25 years - Check that if the first unit has only 1 digit, a second unit is used.
C
cactusman 已提交
118
        assertEquals(Messages.Util_year(9)+ " " + Messages.Util_month(3), Util.getTimeSpanString(291708000000L));
119
        // 67 seconds
C
cactusman 已提交
120
        assertEquals(Messages.Util_minute(1) + " " + Messages.Util_second(7), Util.getTimeSpanString(67000L));
121
        // 17 seconds - Check that times less than a minute only use seconds.
C
cactusman 已提交
122
        assertEquals(Messages.Util_second(17), Util.getTimeSpanString(17000L));
123 124 125 126 127
        // 1712ms -> 1.7sec
        assertEquals(Messages.Util_second(1.7), Util.getTimeSpanString(1712L));
        // 171ms -> 0.17sec
        assertEquals(Messages.Util_second(0.17), Util.getTimeSpanString(171L));
        // 101ms -> 0.10sec
128
        assertEquals(Messages.Util_second(0.1), Util.getTimeSpanString(101L));
129 130 131 132
        // 17ms
        assertEquals(Messages.Util_millisecond(17), Util.getTimeSpanString(17L));
        // 1ms
        assertEquals(Messages.Util_millisecond(1), Util.getTimeSpanString(1L));
133 134 135 136 137 138 139 140 141
        // Test HUDSON-2843 (locale with comma as fraction separator got exception for <10 sec)
        Locale saveLocale = Locale.getDefault();
        Locale.setDefault(Locale.GERMANY);
        try {
            // Just verifying no exception is thrown:
            assertNotNull("German locale", Util.getTimeSpanString(1234));
            assertNotNull("German locale <1 sec", Util.getTimeSpanString(123));
        }
        finally { Locale.setDefault(saveLocale); }
142
    }
C
cactusman 已提交
143

144 145 146 147

    /**
     * Test that Strings that contain spaces are correctly URL encoded.
     */
148
    @Test
149 150 151 152 153
    public void testEncodeSpaces() {
        final String urlWithSpaces = "http://hudson/job/Hudson Job";
        String encoded = Util.encode(urlWithSpaces);
        assertEquals(encoded, "http://hudson/job/Hudson%20Job");
    }
154

155 156 157
    /**
     * Test the rawEncode() method.
     */
158
    @Test
159 160 161 162
    public void testRawEncode() {
        String[] data = {  // Alternating raw,encoded
            "abcdefghijklmnopqrstuvwxyz", "abcdefghijklmnopqrstuvwxyz",
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
163
            "01234567890!@$&*()-_=+',.", "01234567890!@$&*()-_=+',.",
164 165
            " \"#%/:;<>?", "%20%22%23%25%2F%3A%3B%3C%3E%3F",
            "[\\]^`{|}~", "%5B%5C%5D%5E%60%7B%7C%7D%7E",
166
            "d\u00E9velopp\u00E9s", "d%C3%A9velopp%C3%A9s",
167 168 169 170 171 172
        };
        for (int i = 0; i < data.length; i += 2) {
            assertEquals("test " + i, data[i + 1], Util.rawEncode(data[i]));
        }
    }

173 174 175
    /**
     * Test the tryParseNumber() method.
     */
176
    @Test
177 178 179 180 181 182
    public void testTryParseNumber() {
        assertEquals("Successful parse did not return the parsed value", 20, Util.tryParseNumber("20", 10).intValue());
        assertEquals("Failed parse did not return the default value", 10, Util.tryParseNumber("ss", 10).intValue());
        assertEquals("Parsing empty string did not return the default value", 10, Util.tryParseNumber("", 10).intValue());
        assertEquals("Parsing null string did not return the default value", 10, Util.tryParseNumber(null, 10).intValue());
    }
K
kohsuke 已提交
183

184
    @Test
K
kohsuke 已提交
185
    public void testSymlink() throws Exception {
186
        Assume.assumeTrue(!Functions.isWindows());
K
kohsuke 已提交
187

188 189
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        StreamTaskListener l = new StreamTaskListener(baos);
190
        File d = tmp.getRoot();
K
kohsuke 已提交
191 192
        try {
            new FilePath(new File(d, "a")).touch(0);
193
            assertNull(Util.resolveSymlink(new File(d, "a")));
K
kohsuke 已提交
194
            Util.createSymlink(d,"a","x", l);
195
            assertEquals("a",Util.resolveSymlink(new File(d,"x")));
K
kohsuke 已提交
196 197 198 199

            // test a long name
            StringBuilder buf = new StringBuilder(768);
            for( int i=0; i<768; i++)
200
                buf.append((char)('0'+(i%10)));
K
kohsuke 已提交
201
            Util.createSymlink(d,buf.toString(),"x", l);
202 203

            String log = baos.toString();
204
            if (log.length() > 0)
205 206
                System.err.println("log output: " + log);

207
            assertEquals(buf.toString(),Util.resolveSymlink(new File(d,"x")));
208 209


210 211 212
            // test linking from another directory
            File anotherDir = new File(d,"anotherDir");
            assertTrue("Couldn't create "+anotherDir,anotherDir.mkdir());
213

214
            Util.createSymlink(d,"a","anotherDir/link",l);
215
            assertEquals("a",Util.resolveSymlink(new File(d,"anotherDir/link")));
216 217

            // JENKINS-12331: either a bug in createSymlink or this isn't supposed to work:
218
            //assertTrue(Util.isSymlink(new File(d,"anotherDir/link")));
219 220 221 222 223 224 225 226

            File external = File.createTempFile("something", "");
            try {
                Util.createSymlink(d, external.getAbsolutePath(), "outside", l);
                assertEquals(external.getAbsolutePath(), Util.resolveSymlink(new File(d, "outside")));
            } finally {
                assertTrue(external.delete());
            }
227 228 229 230
        } finally {
            Util.deleteRecursive(d);
        }
    }
231

232
    @Test
233 234
    public void testIsSymlink() throws IOException, InterruptedException {
        Assume.assumeTrue(!Functions.isWindows());
235

236 237
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        StreamTaskListener l = new StreamTaskListener(baos);
238
        File d = tmp.getRoot();
239 240 241 242
        try {
            new FilePath(new File(d, "original")).touch(0);
            assertFalse(Util.isSymlink(new File(d, "original")));
            Util.createSymlink(d,"original","link", l);
243

244
            assertTrue(Util.isSymlink(new File(d, "link")));
245

246 247 248 249
            // test linking to another directory
            File dir = new File(d,"dir");
            assertTrue("Couldn't create "+dir,dir.mkdir());
            assertFalse(Util.isSymlink(new File(d,"dir")));
250

251 252
            File anotherDir = new File(d,"anotherDir");
            assertTrue("Couldn't create "+anotherDir,anotherDir.mkdir());
253

254 255 256
            Util.createSymlink(d,"dir","anotherDir/symlinkDir",l);
            // JENKINS-12331: either a bug in createSymlink or this isn't supposed to work:
            // assertTrue(Util.isSymlink(new File(d,"anotherDir/symlinkDir")));
K
kohsuke 已提交
257
        } finally {
258 259 260 261
            Util.deleteRecursive(d);
        }
    }

262 263 264 265 266 267 268 269 270 271 272
    @Test
    public void testDeleteFile() throws Exception {
        File f = tmp.newFile();
        // Test: File is deleted
        mkfiles(f);
        Util.deleteFile(f);
        assertFalse("f exists after calling Util.deleteFile", f.exists());
    }

    @Test
    public void testDeleteFile_onWindows() throws Exception {
273 274 275 276 277 278 279
        Assume.assumeTrue(Functions.isWindows());
        Class<?> c;
        try {
            c = Class.forName("java.nio.file.FileSystemException");
        } catch (ClassNotFoundException x) {
            throw new AssumptionViolatedException("prior to JDK 7", x);
        }
280
        final int defaultDeletionMax = Util.DELETION_MAX;
281
        try {
282 283 284 285
            File f = tmp.newFile();
            // Test: If we cannot delete a file, we throw explaining why
            mkfiles(f);
            lockFileForDeletion(f);
286
            Util.DELETION_MAX = 1;
287 288 289 290
            try {
                Util.deleteFile(f);
                fail("should not have been deletable");
            } catch (IOException x) {
291 292
                assertThat(calcExceptionHierarchy(x), hasItem(c));
                assertThat(x.getMessage(), containsString(f.getPath()));
293 294
            }
        } finally {
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
            Util.DELETION_MAX = defaultDeletionMax;
            unlockFilesForDeletion();
        }
    }

    @Test
    public void testDeleteContentsRecursive() throws Exception {
        final File dir = tmp.newFolder();
        final File d1 = new File(dir, "d1");
        final File d2 = new File(dir, "d2");
        final File f1 = new File(dir, "f1");
        final File d1f1 = new File(d1, "d1f1");
        final File d2f2 = new File(d2, "d1f2");
        // Test: Files and directories are deleted
        mkdirs(dir, d1, d2);
        mkfiles(f1, d1f1, d2f2);
        Util.deleteContentsRecursive(dir);
        assertTrue("dir exists", dir.exists());
        assertFalse("d1 exists", d1.exists());
        assertFalse("d2 exists", d2.exists());
        assertFalse("f1 exists", f1.exists());
    }

    @Test
    public void testDeleteContentsRecursive_onWindows() throws Exception {
        Assume.assumeTrue(Functions.isWindows());
        final File dir = tmp.newFolder();
        final File d1 = new File(dir, "d1");
        final File d2 = new File(dir, "d2");
        final File f1 = new File(dir, "f1");
        final File d1f1 = new File(d1, "d1f1");
        final File d2f2 = new File(d2, "d1f2");
        final int defaultDeletionMax = Util.DELETION_MAX;
        final int defaultDeletionWait = Util.WAIT_BETWEEN_DELETION_RETRIES;
        final boolean defaultDeletionGC = Util.GC_AFTER_FAILED_DELETE;
        try {
            // Test: If we cannot delete a file, we throw
            // but still deletes everything it can
            // even if we are not retrying deletes.
            mkdirs(dir, d1, d2);
            mkfiles(f1, d1f1, d2f2);
            lockFileForDeletion(d1f1);
            Util.GC_AFTER_FAILED_DELETE = false;
            Util.DELETION_MAX = 2;
            Util.WAIT_BETWEEN_DELETION_RETRIES = 0;
            try {
                Util.deleteContentsRecursive(dir);
                fail("Expected IOException");
            } catch (IOException x) {
                assertFalse("d2 should not exist", d2.exists());
                assertFalse("f1 should not exist", f1.exists());
                assertFalse("d1f2 should not exist", d2f2.exists());
                assertThat(x.getMessage(), containsString(dir.getPath()));
                assertThat(x.getMessage(), allOf(not(containsString("interrupted")), containsString("Tried 2 times (of a maximum of 2)."), not(containsString("garbage-collecting")), not(containsString("wait"))));
            }
        } finally {
            Util.DELETION_MAX = defaultDeletionMax;
            Util.WAIT_BETWEEN_DELETION_RETRIES = defaultDeletionWait;
            Util.GC_AFTER_FAILED_DELETE = defaultDeletionGC;
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
            unlockFilesForDeletion();
        }
    }

    @Test
    public void testDeleteRecursive() throws Exception {
        final File dir = tmp.newFolder();
        final File d1 = new File(dir, "d1");
        final File d2 = new File(dir, "d2");
        final File f1 = new File(dir, "f1");
        final File d1f1 = new File(d1, "d1f1");
        final File d2f2 = new File(d2, "d1f2");
        // Test: Files and directories are deleted
        mkdirs(dir, d1, d2);
        mkfiles(f1, d1f1, d2f2);
        Util.deleteRecursive(dir);
        assertFalse("dir exists", dir.exists());
    }

    @Test
    public void testDeleteRecursive_onWindows() throws Exception {
        Assume.assumeTrue(Functions.isWindows());
        final File dir = tmp.newFolder();
        final File d1 = new File(dir, "d1");
        final File d2 = new File(dir, "d2");
        final File f1 = new File(dir, "f1");
        final File d1f1 = new File(d1, "d1f1");
        final File d2f2 = new File(d2, "d1f2");
382 383 384
        final int defaultDeletionMax = Util.DELETION_MAX;
        final int defaultDeletionWait = Util.WAIT_BETWEEN_DELETION_RETRIES;
        final boolean defaultDeletionGC = Util.GC_AFTER_FAILED_DELETE;
385 386
        try {
            // Test: If we cannot delete a file, we throw
387 388 389 390
            // but still deletes everything it can
            // even if we are not retrying deletes.
        	// (And when we are not retrying deletes,
        	// we do not do the "between retries" delay)
391 392 393
            mkdirs(dir, d1, d2);
            mkfiles(f1, d1f1, d2f2);
            lockFileForDeletion(d1f1);
394 395 396 397
            Util.GC_AFTER_FAILED_DELETE = false;
            Util.DELETION_MAX = 1;
            Util.WAIT_BETWEEN_DELETION_RETRIES = 10000; // long enough to notice
            long timeWhenDeletionStarted = System.currentTimeMillis();
398 399 400 401
            try {
                Util.deleteRecursive(dir);
                fail("Expected IOException");
            } catch (IOException x) {
402
                long timeWhenDeletionEnded = System.currentTimeMillis();
403 404 405
                assertTrue("dir exists", dir.exists());
                assertTrue("d1 exists", d1.exists());
                assertTrue("d1f1 exists", d1f1.exists());
406 407 408
                assertFalse("d2 should not exist", d2.exists());
                assertFalse("f1 should not exist", f1.exists());
                assertFalse("d1f2 should not exist", d2f2.exists());
409
                assertThat(x.getMessage(), containsString(dir.getPath()));
410 411 412
                assertThat(x.getMessage(), allOf(not(containsString("interrupted")), not(containsString("maximum of")), not(containsString("garbage-collecting"))));
                long actualTimeSpentDeleting = timeWhenDeletionEnded - timeWhenDeletionStarted;
                assertTrue("did not wait - took " + actualTimeSpentDeleting + "ms", actualTimeSpentDeleting<1000L);
413
            }
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
            unlockFileForDeletion(d1f1);
            // Deletes get retried if they fail 1st time around,
            // allowing the operation to succeed on subsequent attempts.
            // Note: This is what bug JENKINS-15331 is all about.
            mkdirs(dir, d1, d2);
            mkfiles(f1, d1f1, d2f2);
            lockFileForDeletion(d2f2);
            Util.DELETION_MAX=4;
            Util.WAIT_BETWEEN_DELETION_RETRIES = 100;
            Thread unlockAfterDelay = new Thread("unlockFileAfterDelay") {
                public void run() {
                    try {
                        Thread.sleep(Util.WAIT_BETWEEN_DELETION_RETRIES);
                        unlockFileForDeletion(d2f2);
                    } catch( Exception x ) { /* ignored */ }
                }
            };
            unlockAfterDelay.start();
            Util.deleteRecursive(dir);
            assertFalse("dir should have been deleted", dir.exists());
            unlockAfterDelay.join();
            // An interrupt aborts the delete and makes it fail, even
            // if we had been told to retry a lot.
            mkdirs(dir, d1, d2);
            mkfiles(f1, d1f1, d2f2);
            lockFileForDeletion(d1f1);
            Util.DELETION_MAX=10;
            Util.WAIT_BETWEEN_DELETION_RETRIES = -1000;
            Util.GC_AFTER_FAILED_DELETE = true;
            final AtomicReference<Throwable> thrown = new AtomicReference<Throwable>();
444
            Thread deleteToBeInterrupted = new Thread("deleteToBeInterrupted") {
445 446 447 448 449
                public void run() {
                    try { Util.deleteRecursive(dir); }
                    catch( Throwable x ) { thrown.set(x); }
                }
            };
450 451 452 453
            deleteToBeInterrupted.start();
            deleteToBeInterrupted.interrupt();
            deleteToBeInterrupted.join(500);
            assertFalse("deletion stopped", deleteToBeInterrupted.isAlive());
454 455 456 457 458
            assertTrue("d1f1 still exists", d1f1.exists());
            unlockFileForDeletion(d1f1);
            Throwable deletionInterruptedEx = thrown.get();
            assertThat(deletionInterruptedEx, instanceOf(IOException.class));
            assertThat(deletionInterruptedEx.getMessage(), allOf(containsString("interrupted"), containsString("maximum of " + Util.DELETION_MAX), containsString("garbage-collecting")));
459
        } finally {
460 461 462
            Util.DELETION_MAX = defaultDeletionMax;
            Util.WAIT_BETWEEN_DELETION_RETRIES = defaultDeletionWait;
            Util.GC_AFTER_FAILED_DELETE = defaultDeletionGC;
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490
            unlockFilesForDeletion();
        }
    }

    /** Creates multiple directories. */
    private static void mkdirs(File... dirs) {
        for( File d : dirs ) {
            d.mkdir();
            assertTrue(d.getPath(), d.isDirectory());
        }
    }

    /** Creates multiple files, each containing their filename as text content. */
    private static void mkfiles(File... files) throws IOException {
        for( File f : files )
            FileUtils.write(f, f.getName());
    }

    /** Means of unlocking all the files we have locked, indexed by {@link File}. */
    private final Map<File, Callable<Void>> unlockFileCallables = new HashMap<File, Callable<Void>>();

    /** Prevents a file from being deleted, so we can stress the deletion code's retries. */
    private void lockFileForDeletion(File f) throws IOException, InterruptedException {
        assert !unlockFileCallables.containsKey(f) : f + " is already locked." ;
        // Limitation: Only works on Windows. On unix we can delete anything we can create.
        // On unix, can't use "chmod a-w" on the dir as the code-under-test undoes that.
        // On unix, can't use "chattr +i" because that needs root.
        // On unix, can't use "chattr +u" because ext fs ignores it.
491
        // On Windows, can't use FileChannel.lock() because that doesn't block deletion
492
        // On Windows, we can't delete files that are open for reading, so we use that.
493 494 495
        // NOTE: This is a hack in any case as there is no guarantee that all Windows filesystems
        // will enforce blocking deletion on open files... just that the ones we normally
        // test with seem to block.
496
        assert Functions.isWindows();
497
        final InputStream s = new FileInputStream(f); // intentional use of FileInputStream
498
        unlockFileCallables.put(f, new Callable<Void>() {
499
            public Void call() throws IOException { s.close(); return null; };
500 501 502 503 504 505 506 507 508 509 510 511
        });
    }

    /** Undoes a call to {@link #lockFileForDeletion(File)}. */
    private void unlockFileForDeletion(File f) throws Exception {
        unlockFileCallables.remove(f).call();
    }

    /** Undoes all calls to {@link #lockFileForDeletion(File)}. */
    private void unlockFilesForDeletion() throws Exception {
        while( !unlockFileCallables.isEmpty() ) {
            unlockFileForDeletion(unlockFileCallables.keySet().iterator().next());
K
kohsuke 已提交
512 513
        }
    }
S
Seiji Sogabe 已提交
514

515 516 517 518 519 520 521 522
    /** Returns all classes in the exception hierarchy. */
    private static Iterable<Class<?>> calcExceptionHierarchy(Throwable t) {
        final List<Class<?>> result = Lists.newArrayList();
        for( ; t!=null ; t = t.getCause())
            result.add(t.getClass());
        return result;
    }

523 524
    @Test
    public void testHtmlEscape() {
S
Seiji Sogabe 已提交
525
        assertEquals("<br>", Util.escape("\n"));
526
        assertEquals("&lt;a&gt;", Util.escape("<a>"));
527
        assertEquals("&#039;&quot;", Util.escape("'\""));
S
Seiji Sogabe 已提交
528 529
        assertEquals("&nbsp; ", Util.escape("  "));
    }
530

531 532 533 534
    /**
     * Compute 'known-correct' digests and see if I still get them when computed concurrently
     * to another digest.
     */
535
    @Issue("JENKINS-10346")
536
    @Test
537 538 539
    public void testDigestThreadSafety() throws InterruptedException {
    	String a = "abcdefgh";
    	String b = "123456789";
540

541 542
    	String digestA = Util.getDigestOf(a);
    	String digestB = Util.getDigestOf(b);
543

544 545
    	DigesterThread t1 = new DigesterThread(a, digestA);
    	DigesterThread t2 = new DigesterThread(b, digestB);
546

547 548
    	t1.start();
    	t2.start();
549

550 551
    	t1.join();
    	t2.join();
552

553 554 555 556 557 558 559
    	if (t1.error != null) {
    		fail(t1.error);
    	}
    	if (t2.error != null) {
    		fail(t2.error);
    	}
    }
560

561 562 563
    private static class DigesterThread extends Thread {
    	private String string;
		private String expectedDigest;
564

565 566 567 568 569 570
		private String error;

		public DigesterThread(String string, String expectedDigest) {
    		this.string = string;
    		this.expectedDigest = expectedDigest;
    	}
571

572 573 574 575 576 577 578 579 580 581
		public void run() {
			for (int i=0; i < 1000; i++) {
				String digest = Util.getDigestOf(this.string);
				if (!this.expectedDigest.equals(digest)) {
					this.error = "Expected " + this.expectedDigest + ", but got " + digest;
					break;
				}
			}
		}
    }
K
Kohsuke Kawaguchi 已提交
582

583
    @Test
K
Kohsuke Kawaguchi 已提交
584 585 586 587 588 589 590 591 592
    public void testIsAbsoluteUri() {
        assertTrue(Util.isAbsoluteUri("http://foobar/"));
        assertTrue(Util.isAbsoluteUri("mailto:kk@kohsuke.org"));
        assertTrue(Util.isAbsoluteUri("d123://test/"));
        assertFalse(Util.isAbsoluteUri("foo/bar/abc:def"));
        assertFalse(Util.isAbsoluteUri("foo?abc:def"));
        assertFalse(Util.isAbsoluteUri("foo#abc:def"));
        assertFalse(Util.isAbsoluteUri("foo/bar"));
    }
593

594 595 596 597 598 599 600 601 602 603 604 605
    @Test
    @Issue("SECURITY-276")
    public void testIsSafeToRedirectTo() {
        assertFalse(Util.isSafeToRedirectTo("http://foobar/"));
        assertFalse(Util.isSafeToRedirectTo("mailto:kk@kohsuke.org"));
        assertFalse(Util.isSafeToRedirectTo("d123://test/"));
        assertFalse(Util.isSafeToRedirectTo("//google.com"));

        assertTrue(Util.isSafeToRedirectTo("foo/bar/abc:def"));
        assertTrue(Util.isSafeToRedirectTo("foo?abc:def"));
        assertTrue(Util.isSafeToRedirectTo("foo#abc:def"));
        assertTrue(Util.isSafeToRedirectTo("foo/bar"));
606 607 608 609 610 611
        assertTrue(Util.isSafeToRedirectTo("/"));
        assertTrue(Util.isSafeToRedirectTo("/foo"));
        assertTrue(Util.isSafeToRedirectTo(".."));
        assertTrue(Util.isSafeToRedirectTo("../.."));
        assertTrue(Util.isSafeToRedirectTo("/#foo"));
        assertTrue(Util.isSafeToRedirectTo("/?foo"));
612 613
    }

614 615 616 617 618 619 620 621 622
    @Test
    public void loadProperties() throws IOException {

        assertEquals(0, Util.loadProperties("").size());

        Properties p = Util.loadProperties("k.e.y=va.l.ue");
        assertEquals(p.toString(), "va.l.ue", p.get("k.e.y"));
        assertEquals(p.toString(), 1, p.size());
    }
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674

    @Test
    public void isRelativePathUnix() {
        assertThat("/", not(aRelativePath()));
        assertThat("/foo/bar", not(aRelativePath()));
        assertThat("/foo/../bar", not(aRelativePath()));
        assertThat("", aRelativePath());
        assertThat(".", aRelativePath());
        assertThat("..", aRelativePath());
        assertThat("./foo", aRelativePath());
        assertThat("./foo/bar", aRelativePath());
        assertThat("./foo/bar/", aRelativePath());
    }

    @Test
    public void isRelativePathWindows() {
        assertThat("\\", aRelativePath());
        assertThat("\\foo\\bar", aRelativePath());
        assertThat("\\foo\\..\\bar", aRelativePath());
        assertThat("", aRelativePath());
        assertThat(".", aRelativePath());
        assertThat(".\\foo", aRelativePath());
        assertThat(".\\foo\\bar", aRelativePath());
        assertThat(".\\foo\\bar\\", aRelativePath());
        assertThat("\\\\foo", aRelativePath());
        assertThat("\\\\foo\\", not(aRelativePath()));
        assertThat("\\\\foo\\c", not(aRelativePath()));
        assertThat("C:", aRelativePath());
        assertThat("z:", aRelativePath());
        assertThat("0:", aRelativePath());
        assertThat("c:.", aRelativePath());
        assertThat("c:\\", not(aRelativePath()));
        assertThat("c:/", not(aRelativePath()));
    }

    private static RelativePathMatcher aRelativePath() {
        return new RelativePathMatcher();
    }

    private static class RelativePathMatcher extends BaseMatcher<String> {

        @Override
        public boolean matches(Object item) {
            return Util.isRelativePath((String) item);
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("a relative path");
        }
    }

K
kohsuke 已提交
675
}