/* * The MIT License * * Copyright (c) 2012, 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 jenkins.model.lazy; import java.io.File; import static org.junit.Assert.*; import jenkins.model.lazy.AbstractLazyLoadRunMap.Direction; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Set; import java.util.SortedMap; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import jenkins.util.Timer; import org.junit.BeforeClass; import org.jvnet.hudson.test.Issue; /** * @author Kohsuke Kawaguchi */ public class AbstractLazyLoadRunMapTest { // A=1, B=3, C=5 @Rule public FakeMapBuilder aBuilder = new FakeMapBuilder(); private FakeMap a; // empty map @Rule public FakeMapBuilder bBuilder = new FakeMapBuilder(); private FakeMap b; @Rule public FakeMapBuilder localBuilder = new FakeMapBuilder(); @Rule public FakeMapBuilder localExpiredBuilder = new FakeMapBuilder() { @Override public FakeMap make() { assert getDir()!=null; return new FakeMap(getDir()) { @Override protected BuildReference<Build> createReference(Build r) { return new BuildReference<Build>(Integer.toString(r.n), /* pretend referent expired */ null); } }; } }; private final Map<Integer,Semaphore> slowBuilderStartSemaphores = new HashMap<>(); private final Map<Integer,Semaphore> slowBuilderEndSemaphores = new HashMap<>(); private final Map<Integer,AtomicInteger> slowBuilderLoadCount = new HashMap<>(); @Rule public FakeMapBuilder slowBuilder = new FakeMapBuilder() { @Override public FakeMap make() { return new FakeMap(getDir()) { @Override protected Build retrieve(File dir) throws IOException { Build b = super.retrieve(dir); slowBuilderStartSemaphores.get(b.n).release(); try { slowBuilderEndSemaphores.get(b.n).acquire(); } catch (InterruptedException x) { throw new IOException(x); } slowBuilderLoadCount.get(b.n).incrementAndGet(); return b; } }; } }; @BeforeClass public static void setUpClass() { AbstractLazyLoadRunMap.LOGGER.setLevel(Level.OFF); } @Before public void setUp() throws Exception { a = aBuilder.add(1).add(3).add(5).make(); b = bBuilder.make(); } @Test public void lookup() { assertNull(a.get(0)); a.get(1).asserts(1); assertNull(a.get(2)); a.get(3).asserts(3); assertNull(a.get(4)); a.get(5).asserts(5); assertNull(a.get(6)); assertNull(b.get(1)); assertNull(b.get(3)); assertNull(b.get(5)); } @Test public void lookup2() { assertNull(a.get(6)); } @Test public void idempotentLookup() { for (int i=0; i<5; i++) { a.get(1).asserts(1); a.get((Object)1).asserts(1); } } @Test public void lookupWithBogusKeyType() { assertNull(a.get(null)); assertNull(a.get("foo")); assertNull(a.get(this)); } @Test public void firstKey() { assertEquals(5, a.firstKey().intValue()); try { b.firstKey(); fail(); } catch (NoSuchElementException e) { // as expected } } @Issue("JENKINS-26690") @Test public void headMap() { assertEquals("[]", a.headMap(Integer.MAX_VALUE).keySet().toString()); assertEquals("[]", a.headMap(6).keySet().toString()); assertEquals("[]", a.headMap(5).keySet().toString()); assertEquals("[5]", a.headMap(4).keySet().toString()); assertEquals("[5]", a.headMap(3).keySet().toString()); assertEquals("[5, 3]", a.headMap(2).keySet().toString()); assertEquals("[5, 3]", a.headMap(1).keySet().toString()); assertEquals("[5, 3, 1]", a.headMap(0).keySet().toString()); assertEquals("[5, 3, 1]", a.headMap(-1).keySet().toString()); assertEquals("[5, 3, 1]", a.headMap(-2).keySet().toString()); // this failed assertEquals("[5, 3, 1]", a.headMap(Integer.MIN_VALUE).keySet().toString()); } @Test public void lastKey() { assertEquals(1, a.lastKey().intValue()); try { b.lastKey(); fail(); } catch (NoSuchElementException e) { // as expected } } @Test public void search() { // searching toward non-existent direction assertNull(a.search(99, Direction.ASC)); assertNull(a.search(-99, Direction.DESC)); } @Issue("JENKINS-19418") @Test public void searchExactWhenIndexedButSoftReferenceExpired() throws IOException { final FakeMap m = localExpiredBuilder.add(1).add(2).make(); // force index creation m.entrySet(); m.search(1, Direction.EXACT).asserts(1); assertNull(m.search(3, Direction.EXACT)); assertNull(m.search(0, Direction.EXACT)); } @Issue("JENKINS-22681") @Test public void exactSearchShouldNotReload() throws Exception { FakeMap m = localBuilder.add(1).add(2).make(); assertNull(m.search(0, Direction.EXACT)); Build a = m.search(1, Direction.EXACT); a.asserts(1); Build b = m.search(2, Direction.EXACT); b.asserts(2); assertNull(m.search(0, Direction.EXACT)); assertSame(a, m.search(1, Direction.EXACT)); assertSame(b, m.search(2, Direction.EXACT)); assertNull(m.search(3, Direction.EXACT)); assertNull(m.search(0, Direction.EXACT)); assertSame(a, m.search(1, Direction.EXACT)); assertSame("#2 should not have been reloaded by searching for #3", b, m.search(2, Direction.EXACT)); assertNull(m.search(3, Direction.EXACT)); } /** * If load fails, search needs to gracefully handle it */ @Test public void unloadableData() throws IOException { FakeMap m = localBuilder.add(1).addUnloadable(3).add(5).make(); assertNull(m.search(3, Direction.EXACT)); m.search(3,Direction.DESC).asserts(1); m.search(3, Direction.ASC ).asserts(5); } @Test public void eagerLoading() throws IOException { Map.Entry[] b = a.entrySet().toArray(new Map.Entry[3]); ((Build)b[0].getValue()).asserts(5); ((Build)b[1].getValue()).asserts(3); ((Build)b[2].getValue()).asserts(1); } @Test public void fastSubMap() throws Exception { SortedMap<Integer,Build> m = a.subMap(99, 2); assertEquals(2, m.size()); Build[] b = m.values().toArray(new Build[2]); assertEquals(2, b.length); b[0].asserts(5); b[1].asserts(3); } @Test public void identity() { assertTrue(a.equals(a)); assertTrue(!a.equals(b)); a.hashCode(); b.hashCode(); } @Issue("JENKINS-15439") @Test public void indexOutOfBounds() throws Exception { FakeMapBuilder f = localBuilder; f.add(100) .addUnloadable(150) .addUnloadable(151) .addUnloadable(152) .addUnloadable(153) .addUnloadable(154) .addUnloadable(155) .add(200) .add(201); FakeMap map = f.make(); Build x = map.search(Integer.MAX_VALUE, Direction.DESC); assert x.n==201; } @Issue("JENKINS-18065") @Test public void all() throws Exception { assertEquals("[]", a.getLoadedBuilds().keySet().toString()); Set<Map.Entry<Integer,Build>> entries = a.entrySet(); assertEquals("[]", a.getLoadedBuilds().keySet().toString()); assertFalse(entries.isEmpty()); assertEquals("5 since it is the latest", "[5]", a.getLoadedBuilds().keySet().toString()); assertEquals(5, a.getById("5").n); assertEquals("[5]", a.getLoadedBuilds().keySet().toString()); assertEquals(1, a.getByNumber(1).n); assertEquals("[5, 1]", a.getLoadedBuilds().keySet().toString()); a.purgeCache(); assertEquals("[]", a.getLoadedBuilds().keySet().toString()); Iterator<Map.Entry<Integer,Build>> iterator = entries.iterator(); assertEquals("[5]", a.getLoadedBuilds().keySet().toString()); assertTrue(iterator.hasNext()); assertEquals("[5]", a.getLoadedBuilds().keySet().toString()); Map.Entry<Integer,Build> entry = iterator.next(); assertEquals("[5, 3]", a.getLoadedBuilds().keySet().toString()); assertEquals(5, entry.getKey().intValue()); assertEquals("[5, 3]", a.getLoadedBuilds().keySet().toString()); assertEquals(5, entry.getValue().n); assertEquals("[5, 3]", a.getLoadedBuilds().keySet().toString()); assertTrue(iterator.hasNext()); entry = iterator.next(); assertEquals(3, entry.getKey().intValue()); assertEquals(".next() precomputes the one after that too", "[5, 3, 1]", a.getLoadedBuilds().keySet().toString()); assertEquals(3, entry.getValue().n); assertEquals("[5, 3, 1]", a.getLoadedBuilds().keySet().toString()); assertTrue(iterator.hasNext()); entry = iterator.next(); assertEquals(1, entry.getKey().intValue()); assertEquals("[5, 3, 1]", a.getLoadedBuilds().keySet().toString()); assertEquals(1, entry.getValue().n); assertEquals("[5, 3, 1]", a.getLoadedBuilds().keySet().toString()); assertFalse(iterator.hasNext()); } @Issue("JENKINS-18065") @Test public void entrySetIterator() { Iterator<Entry<Integer, Build>> itr = a.entrySet().iterator(); // iterator, when created fresh, shouldn't force loading everything // this involves binary searching, so it can load several. assertTrue(a.getLoadedBuilds().size() < 3); // check if the first entry is legit assertTrue(itr.hasNext()); Entry<Integer, Build> e = itr.next(); assertEquals((Integer)5,e.getKey()); e.getValue().asserts(5); // now that the first entry is returned, we expect there to be two loaded assertTrue(a.getLoadedBuilds().size() < 3); // check if the second entry is legit assertTrue(itr.hasNext()); e = itr.next(); assertEquals((Integer)3, e.getKey()); e.getValue().asserts(3); // repeat the process for the third one assertTrue(a.getLoadedBuilds().size() <= 3); // check if the third entry is legit assertTrue(itr.hasNext()); e = itr.next(); assertEquals((Integer) 1, e.getKey()); e.getValue().asserts(1); assertFalse(itr.hasNext()); assertEquals(3, a.getLoadedBuilds().size()); } @Issue("JENKINS-18065") @Test public void entrySetEmpty() { // entrySet().isEmpty() shouldn't cause full data load assertFalse(a.entrySet().isEmpty()); assertTrue(a.getLoadedBuilds().size() < 3); } @Issue("JENKINS-18065") @Test public void entrySetSize() { assertEquals(3, a.entrySet().size()); assertEquals(0, b.entrySet().size()); } @Issue("JENKINS-25655") @Test public void entrySetChanges() { assertEquals(3, a.entrySet().size()); a.put(new Build(7)); assertEquals(4, a.entrySet().size()); } @Issue("JENKINS-18065") @Test public void entrySetContains() { for (Entry<Integer, Build> e : a.entrySet()) { assertTrue(a.entrySet().contains(e)); } } @Issue("JENKINS-22767") @Test public void slowRetrieve() throws Exception { for (int i = 1; i <= 3; i++) { slowBuilder.add(i); slowBuilderStartSemaphores.put(i, new Semaphore(0)); slowBuilderEndSemaphores.put(i, new Semaphore(0)); slowBuilderLoadCount.put(i, new AtomicInteger()); } final FakeMap m = slowBuilder.make(); Future<Build> firstLoad = Timer.get().submit(new Callable<Build>() { @Override public Build call() throws Exception { return m.getByNumber(2); } }); Future<Build> secondLoad = Timer.get().submit(new Callable<Build>() { @Override public Build call() throws Exception { return m.getByNumber(2); } }); slowBuilderStartSemaphores.get(2).acquire(1); // now one of them is inside retrieve(…); the other is waiting for the lock slowBuilderEndSemaphores.get(2).release(2); // allow both to proceed Build first = firstLoad.get(); Build second = secondLoad.get(); assertEquals(1, slowBuilderLoadCount.get(2).get()); assertSame(second, first); } }