TimeoutMapTest.java

/*
 * Copyright (c) 2008, Harald Kuhr
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.twelvemonkeys.util;

import java.util.*;

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
/**
 * TimeoutMapTest
 * <p/>
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/TimeoutMapTestCase.java#2 $
 */
public class TimeoutMapTest extends MapAbstractTest {

    public Map makeEmptyMap() {
        return new TimeoutMap(60 * 60 * 1000);
    }
    /*
     * The basic Map interface lets one associate keys and values:
     */

    /**
     * Method testBasicMap
     */
    @Test
    public void testBasicMap() {

        Map map = new TimeoutMap(60000L);
        Object key = "key";
        Object value = new Integer(3);

        map.put(key, value);
        assertEquals(value, map.get(key));
    }

    /*
     * If there is no value associated with a key,
     * the basic Map will return null for that key:
     */

    /**
     * Method testBasicMapReturnsNullForMissingKey
     */
    @Test
    public void testBasicMapReturnsNullForMissingKey() {

        Map map = new TimeoutMap(60000L);

        assertNull(map.get("key"));
    }

    /*
     * One can also explicitly store a null value for
     * some key:
     */

    /**
     * Method testBasicMapAllowsNull
     */
    @Test
    public void testBasicMapAllowsNull() {

        Map map = new TimeoutMap(60000L);
        Object key = "key";
        Object value = null;

        map.put(key, value);
        assertNull(map.get(key));
    }

    /**
     * Method testBasicMapAllowsMultipleTypes
     */
    @Test
    public void testBasicMapAllowsMultipleTypes() {

        Map map = new TimeoutMap(60000L);

        map.put("key-1", "value-1");
        map.put(new Integer(2), "value-2");
        map.put("key-3", new Integer(3));
        map.put(new Integer(4), new Integer(4));
        map.put(Boolean.FALSE, "");
        assertEquals("value-1", map.get("key-1"));
        assertEquals("value-2", map.get(new Integer(2)));
        assertEquals(new Integer(3), map.get("key-3"));
        assertEquals(new Integer(4), map.get(new Integer(4)));
        assertEquals("", map.get(Boolean.FALSE));
    }

    /**
     * Method testBasicMapStoresOnlyOneValuePerKey
     */
    @Test
    public void testBasicMapStoresOnlyOneValuePerKey() {

        Map map = new TimeoutMap(60000L);

        assertNull(map.put("key", "value-1"));
        assertEquals("value-1", map.get("key"));
        assertEquals("value-1", map.put("key", "value-2"));
        assertEquals("value-2", map.get("key"));
    }

    /**
     * Method testBasicMapValuesView
     */
    @Test
    public void testBasicMapValuesView() {

        Map map = new TimeoutMap(60000L);

        assertNull(map.put("key-1", new Integer(1)));
        assertNull(map.put("key-2", new Integer(2)));
        assertNull(map.put("key-3", new Integer(3)));
        assertNull(map.put("key-4", new Integer(4)));
        assertEquals(4, map.size());

        Collection values = map.values();
        assertEquals(4, values.size());

        Iterator it = values.iterator();

        assertNotNull(it);
        int count = 0;

        while (it.hasNext()) {
            Object o = it.next();

            assertNotNull(o);
            assertTrue(o instanceof Integer);
            count++;
        }
        assertEquals(4, count);
    }

    /**
     * Method testBasicMapKeySetView
     */
    @Test
    public void testBasicMapKeySetView() {

        Map map = new TimeoutMap(60000L);

        assertNull(map.put("key-1", "value-1"));
        assertNull(map.put("key-2", "value-2"));
        assertNull(map.put("key-3", "value-3"));
        assertNull(map.put("key-4", "value-4"));
        assertEquals(4, map.size());
        Iterator it = map.keySet().iterator();

        assertNotNull(it);
        int count = 0;

        while (it.hasNext()) {
            Object o = it.next();

            assertNotNull(o);
            assertTrue(o instanceof String);
            count++;
        }
        assertEquals(4, count);
    }

    /**
     * Method testBasicMapEntrySetView
     */
    @Test
    public void testBasicMapEntrySetView() {

        Map map = new TimeoutMap(60000L);

        assertNull(map.put("key-1", new Integer(1)));
        assertNull(map.put("key-2", "value-2"));
        assertNull(map.put("key-3", new Object()));
        assertNull(map.put("key-4", Boolean.FALSE));
        assertEquals(4, map.size());
        Iterator it = map.entrySet().iterator();

        assertNotNull(it);
        int count = 0;

        while (it.hasNext()) {
            Object o = it.next();

            assertNotNull(o);
            assertTrue(o instanceof Map.Entry);
            count++;
        }
        assertEquals(4, count);
    }

    /**
     * Method testBasicMapValuesView
     */
    @Test
    public void testBasicMapValuesViewRemoval() {

        Map map = new TimeoutMap(60000L);

        assertNull(map.put("key-1", new Integer(1)));
        assertNull(map.put("key-2", new Integer(2)));
        assertNull(map.put("key-3", new Integer(3)));
        assertNull(map.put("key-4", new Integer(4)));
        assertEquals(4, map.size());
        Iterator it = map.values().iterator();

        assertNotNull(it);
        int count = 0;

        while (it.hasNext()) {
            Object o = it.next();

            assertNotNull(o);
            assertTrue(o instanceof Integer);
            try {
                it.remove();
            }
            catch (UnsupportedOperationException e) {
                fail("Removal failed");
            }
            count++;
        }
        assertEquals(4, count);
    }

    /**
     * Method testBasicMapKeySetView
     */
    @Test
    public void testBasicMapKeySetViewRemoval() {

        Map map = new TimeoutMap(60000L);

        assertNull(map.put("key-1", "value-1"));
        assertNull(map.put("key-2", "value-2"));
        assertNull(map.put("key-3", "value-3"));
        assertNull(map.put("key-4", "value-4"));
        assertEquals(4, map.size());
        Iterator it = map.keySet().iterator();

        assertNotNull(it);
        int count = 0;

        while (it.hasNext()) {
            Object o = it.next();

            assertNotNull(o);
            assertTrue(o instanceof String);
            try {
                it.remove();
            }
            catch (UnsupportedOperationException e) {
                fail("Removal failed");
            }
            count++;
        }
        assertEquals(4, count);
    }

    /**
     * Method testBasicMapEntrySetView
     */
    @Test
    public void testBasicMapEntrySetViewRemoval() {

        Map map = new TimeoutMap(60000L);

        assertNull(map.put("key-1", new Integer(1)));
        assertNull(map.put("key-2", "value-2"));
        assertNull(map.put("key-3", new Object()));
        assertNull(map.put("key-4", Boolean.FALSE));
        assertEquals(4, map.size());
        Iterator it = map.entrySet().iterator();

        assertNotNull(it);
        int count = 0;

        while (it.hasNext()) {
            Object o = it.next();

            assertNotNull(o);
            assertTrue(o instanceof Map.Entry);
            try {
                it.remove();
            }
            catch (UnsupportedOperationException e) {
                fail("Removal failed");
            }
            count++;
        }
        assertEquals(4, count);
    }

    /**
     * Method testBasicMapStoresOnlyOneValuePerKey
     */
    @Test
    public void testTimeoutReturnNull() {

        Map map = new TimeoutMap(100L);

        assertNull(map.put("key", "value-1"));
        assertEquals("value-1", map.get("key"));
        assertNull(map.put("another", "value-2"));
        assertEquals("value-2", map.get("another"));
        synchronized (this) {
            try {
                Thread.sleep(110L);
            }
            catch (InterruptedException e) {
                // Continue, but might break the timeout thing below...
            }
        }

        // Values should now time out
        assertNull(map.get("key"));
        assertNull(map.get("another"));
    }

    /**
     * Method testTimeoutIsEmpty
     */
    @Test
    public void testTimeoutIsEmpty() {

        TimeoutMap map = new TimeoutMap(50L);

        assertNull(map.put("key", "value-1"));
        assertEquals("value-1", map.get("key"));
        assertNull(map.put("another", "value-2"));
        assertEquals("value-2", map.get("another"));
        synchronized (this) {
            try {
                Thread.sleep(100L);
            }
            catch (InterruptedException e) {
                // Continue, but might break the timeout thing below...
            }
        }

        // This for loop should not print anything, if the tests succeed.
        Set set = map.keySet();
        assertEquals(0, set.size());
        for (Iterator iterator = set.iterator(); iterator.hasNext(); System.out.println(iterator.next())) {
            ;
        }
        assertEquals(0, map.size());
        assertTrue(map.isEmpty());
    }

    /**
     * Method testTimeoutWrapIsEmpty
     */
    @Test
    public void testTimeoutWrapIsEmpty() {

        Map map = new TimeoutMap(new LRUMap(2), null, 100L);

        assertNull(map.put("key", "value-1"));
        assertEquals("value-1", map.get("key"));
        assertNull(map.put("another", "value-2"));
        assertEquals("value-2", map.get("another"));
        assertNull(map.put("third", "value-3"));
        assertEquals("value-3", map.get("third"));
        synchronized (this) {
            try {
                Thread.sleep(110L);
            }
            catch (InterruptedException e) {
                // Continue, but might break the timeout thing below...
            }
        }

        // This for loop should not print anything, if the tests succeed.
        Set set = map.keySet();
        assertEquals(0, set.size());
        for (Iterator iterator = set.iterator(); iterator.hasNext(); System.out.println(iterator.next())) {
            ;
        }
        assertEquals(0, map.size());
        assertTrue(map.isEmpty());
    }

    /**
     * Method testTimeoutWrapReturnNull
     */
    @Test
    public void testTimeoutWrapReturnNull() {

        Map map = new TimeoutMap(new LRUMap(), null, 100L);

        assertNull(map.put("key", "value-1"));
        assertEquals("value-1", map.get("key"));
        assertNull(map.put("another", "value-2"));
        assertEquals("value-2", map.get("another"));
        synchronized (this) {
            try {
                Thread.sleep(110L);
            }
            catch (InterruptedException e) {
                // Continue, but might break the timeout thing below...
            }
        }

        // Values should now time out
        assertNull(map.get("key"));
        assertNull(map.get("another"));
    }

    /**
     * Method testWrapMaxSize
     */
    @Test
    public void testWrapMaxSize() {

        LRUMap lru = new LRUMap();

        lru.setMaxSize(2);
        TimeoutMap map = new TimeoutMap(lru, null, 1000L);

        assertNull(map.put("key", "value-1"));
        assertEquals("value-1", map.get("key"));
        assertNull(map.put("another", "value-2"));
        assertEquals("value-2", map.get("another"));
        assertNull(map.put("third", "value-3"));
        assertEquals("value-3", map.get("third"));

        // This value should have expired
        assertNull(map.get("key"));

        // These should be left
        assertEquals("value-2", map.get("another"));
        assertEquals("value-3", map.get("third"));
    }

    /**
     * Method testWrapMapContainingValues
     */
    @Test
    public void testWrapMapContainingValues() {

        Map backing = new TreeMap();

        backing.put("key", "original");
        TimeoutMap map = null;

        try {
            map = new TimeoutMap(backing, backing, 1000L);
            Object value = map.put("key", "value-1");
            assertNotNull(value);  // Should now have value!
            assertEquals("original", value);
        }
        catch (ClassCastException cce) {
            cce.printStackTrace();
            fail("Content not converted to TimedEntries properly!");
        }
        assertEquals("value-1", map.get("key"));
        assertNull(map.put("another", "value-2"));
        assertEquals("value-2", map.get("another"));
        assertNull(map.put("third", "value-3"));
        assertEquals("value-3", map.get("third"));
    }

    @Test
    public void testIteratorRemove() {
        Map map = makeFullMap();

        for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
            iterator.remove();
        }
        assertEquals(0, map.size());

        map = makeFullMap();

        for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
            iterator.next();
            iterator.remove();
        }
        assertEquals(0, map.size());
    }

    @Test
    public void testIteratorPredictableNext() {
        TimeoutMap map = (TimeoutMap) makeFullMap();
        map.setExpiryTime(50l);
        assertFalse(map.isEmpty());

        int count = 0;
        for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
            if (count == 0) {
                // NOTE: Only wait fist time, to avoid slooow tests
                synchronized (this) {
                    try {
                        wait(60l);
                    }
                    catch (InterruptedException e) {
                    }
                }
            }

            try {
                Map.Entry entry = (Map.Entry) iterator.next();
                assertNotNull(entry);
                count++;
            }
            catch (NoSuchElementException nse) {
                fail("Elements expire between Interator.hasNext() and Iterator.next()");
            }
        }

        assertTrue(count > 0, "Elements expired too early, test did not run as expected.");
        //assertEquals("Elements did not expire as expected.", 1, count);
    }

    @Test
    public void testIteratorPredictableRemove() {
        TimeoutMap map = (TimeoutMap) makeFullMap();
        map.setExpiryTime(50l);
        assertFalse(map.isEmpty());

        int count = 0;
        for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
            if (count == 0) {
                // NOTE: Only wait fist time, to avoid slooow tests
                synchronized (this) {
                    try {
                        wait(60L);
                    }
                    catch (InterruptedException e) {
                    }
                }
            }

            try {
                iterator.remove();
                count++;
            }
            catch (NoSuchElementException nse) {
                fail("Elements expired between Interator.hasNext() and Iterator.remove()");
            }
        }

        assertTrue(count > 0, "Elements expired too early, test did not run as expected.");
        //assertEquals("Elements did not expire as expected.", 1, count);
    }

    @Test
    public void testIteratorPredictableNextRemove() {
        TimeoutMap map = (TimeoutMap) makeFullMap();
        map.setExpiryTime(50l);
        assertFalse(map.isEmpty());

        int count = 0;
        for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
            if (count == 0) {
                // NOTE: Only wait fist time, to avoid slooow tests
                synchronized (this) {
                    try {
                        wait(60l);
                    }
                    catch (InterruptedException ignore) {
                    }
                }
            }

            try {
                Map.Entry entry = (Map.Entry) iterator.next();
                assertNotNull(entry);
            }
            catch (NoSuchElementException nse) {
                fail("Elements expired between Interator.hasNext() and Iterator.next()");
            }

            try {
                iterator.remove();
                count++;
            }
            catch (NoSuchElementException nse) {
                fail("Elements expired between Interator.hasNext() and Iterator.remove()");
            }
        }

        assertTrue(count > 0, "Elements expired too early, test did not run as expected.");
        //assertEquals("Elements did not expire as expected.", 1, count);
    }

    @Test
    public void testIteratorPredictableRemovedEntry() {
        TimeoutMap map = (TimeoutMap) makeEmptyMap();
        map.setExpiryTime(1000l); // No elements should expire during this test

        map.put("key-1", new Integer(1));
        map.put("key-2", new Integer(2));

        assertFalse(map.isEmpty());

        Object removedKey = null;
        Object otherKey = null;
        Iterator iterator = map.entrySet().iterator();
        assertTrue(iterator.hasNext(), "Iterator was empty");
        try {
            Map.Entry entry = (Map.Entry) iterator.next();
            assertNotNull(entry);
            removedKey = entry.getKey();
            otherKey = "key-1".equals(removedKey) ? "key-2" : "key-1";
        }
        catch (NoSuchElementException nse) {
            fail("Elements expired between Interator.hasNext() and Iterator.next()");
        }

        try {
            iterator.remove();
        }
        catch (NoSuchElementException nse) {
            fail("Elements expired between Interator.hasNext() and Iterator.remove()");
        }

        assertTrue(!map.containsKey(removedKey), "Wrong entry removed, keySet().iterator() is broken.");
        assertTrue(map.containsKey(otherKey), "Wrong entry removed, keySet().iterator() is broken.");
    }


    @Test
    public void testContainsKeyOnEmptyMap() {
        // See #600
        Map<String, String> timeoutMap = new TimeoutMap<>(30);
        assertFalse(timeoutMap.containsKey("xyz"));
        timeoutMap.put("xyz", "xyz");
        assertTrue(timeoutMap.containsKey("xyz"));

        try {
            Thread.sleep(50); // Let the item expire
        }
        catch (InterruptedException ignore) {
        }

        assertFalse(timeoutMap.containsKey("xyz"));
        assertNull(timeoutMap.get("xyz"));
    }
}