#!/usr/bin/env python
#
# Copyright 2007 Doug Hellmann.
#
#
#                         All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#

"""Unittests for feedcache.cache

"""

__module_id__ = "$Id$"

import logging
logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(levelname)-8s %(name)s %(message)s',
                    )
logger = logging.getLogger('feedcache.test_cache')

#
# Import system modules
#
import copy
import time
import unittest
import UserDict

#
# Import local modules
#
import cache
from test_server import HTTPTestBase, TestHTTPServer

#
# Module
#


class CacheTestBase(HTTPTestBase):

    CACHE_TTL = 30

    def setUp(self):
        HTTPTestBase.setUp(self)

        self.storage = self.getStorage()
        self.cache = cache.Cache(self.storage,
                                 timeToLiveSeconds=self.CACHE_TTL,
                                 userAgent='feedcache.test',
                                 )
        return

    def getStorage(self):
        "Return a cache storage for the test."
        return {}


class CacheTest(CacheTestBase):

    CACHE_TTL = 30

    def getServer(self):
        "These tests do not want to use the ETag or If-Modified-Since headers"
        return TestHTTPServer(applyModifiedHeaders=False)

    def testRetrieveNotInCache(self):
        # Retrieve data not already in the cache.
        feed_data = self.cache.fetch(self.TEST_URL)
        self.failUnless(feed_data)
        self.failUnlessEqual(feed_data.feed.title, 'CacheTest test data')
        return

    def testRetrieveIsInCache(self):
        # Retrieve data which is alread in the cache,
        # and verify that the second copy is identitical
        # to the first.

        # First fetch
        feed_data = self.cache.fetch(self.TEST_URL)

        # Second fetch
        feed_data2 = self.cache.fetch(self.TEST_URL)

        # Since it is the in-memory storage, we should have the
        # exact same object.
        self.failUnless(feed_data is feed_data2)
        return

    def testExpireDataInCache(self):
        # Retrieve data which is in the cache but which
        # has expired and verify that the second copy
        # is different from the first.

        # First fetch
        feed_data = self.cache.fetch(self.TEST_URL)

        # Change the timeout and sleep to move the clock
        self.cache.time_to_live = 0
        time.sleep(1)

        # Second fetch
        feed_data2 = self.cache.fetch(self.TEST_URL)

        # Since we reparsed, the cache response should be different.
        self.failIf(feed_data is feed_data2)
        return

    def testForceUpdate(self):
        # Force cache to retrieve data which is alread in the cache,
        # and verify that the new data is different.

        # Pre-populate the storage with bad data
        self.cache.storage[self.TEST_URL] = (time.time() + 100, self.id())

        # Fetch the data
        feed_data = self.cache.fetch(self.TEST_URL, force_update=True)

        self.failIfEqual(feed_data, self.id())
        return

    def testOfflineMode(self):
        # Retrieve data which is alread in the cache,
        # whether it is expired or not.

        # Pre-populate the storage with data
        self.cache.storage[self.TEST_URL] = (0, self.id())

        # Fetch it
        feed_data = self.cache.fetch(self.TEST_URL, offline=True)

        self.failUnlessEqual(feed_data, self.id())
        return

    def testUnicodeURL(self):
        # Pass in a URL which is unicode

        url = unicode(self.TEST_URL)
        feed_data = self.cache.fetch(url)

        storage = self.cache.storage
        key = unicode(self.TEST_URL).encode('UTF-8')

        # Verify that the storage has a key
        self.failUnless(key in storage)

        # Now pull the data from the storage directly
        storage_timeout, storage_data = self.cache.storage.get(key)
        self.failUnlessEqual(feed_data, storage_data)
        return


class SingleWriteMemoryStorage(UserDict.UserDict):
    """Cache storage which only allows the cache value
    for a URL to be updated one time.
    """

    def __setitem__(self, url, data):
        if url in self.keys():
            modified, existing = self[url]
            # Allow the modified time to change,
            # but not the feed content.
            if data[1] != existing:
                raise AssertionError('Trying to update cache for %s to %s' \
                                         % (url, data))
        UserDict.UserDict.__setitem__(self, url, data)
        return


class CacheConditionalGETTest(CacheTestBase):

    CACHE_TTL = 0

    def getStorage(self):
        return SingleWriteMemoryStorage()

    def testFetchOnceForEtag(self):
        # Fetch data which has a valid ETag value, and verify
        # that while we hit the server twice the response
        # codes cause us to use the same data.

        # First fetch populates the cache
        response1 = self.cache.fetch(self.TEST_URL)
        self.failUnlessEqual(response1.feed.title, 'CacheTest test data')

        # Remove the modified setting from the cache so we know
        # the next time we check the etag will be used
        # to check for updates.  Since we are using an in-memory
        # cache, modifying response1 updates the cache storage
        # directly.
        response1['modified'] = None

        # This should result in a 304 status, and no data from
        # the server.  That means the cache won't try to
        # update the storage, so our SingleWriteMemoryStorage
        # should not raise and we should have the same
        # response object.
        response2 = self.cache.fetch(self.TEST_URL)
        self.failUnless(response1 is response2)

        # Should have hit the server twice
        self.failUnlessEqual(self.server.getNumRequests(), 2)
        return

    def testFetchOnceForModifiedTime(self):
        # Fetch data which has a valid Last-Modified value, and verify
        # that while we hit the server twice the response
        # codes cause us to use the same data.

        # First fetch populates the cache
        response1 = self.cache.fetch(self.TEST_URL)
        self.failUnlessEqual(response1.feed.title, 'CacheTest test data')

        # Remove the etag setting from the cache so we know
        # the next time we check the modified time will be used
        # to check for updates.  Since we are using an in-memory
        # cache, modifying response1 updates the cache storage
        # directly.
        response1['etag'] = None

        # This should result in a 304 status, and no data from
        # the server.  That means the cache won't try to
        # update the storage, so our SingleWriteMemoryStorage
        # should not raise and we should have the same
        # response object.
        response2 = self.cache.fetch(self.TEST_URL)
        self.failUnless(response1 is response2)

        # Should have hit the server twice
        self.failUnlessEqual(self.server.getNumRequests(), 2)
        return


class CacheRedirectHandlingTest(CacheTestBase):

    def _test(self, response):
        # Set up the server to redirect requests,
        # then verify that the cache is not updated
        # for the original or new URL and that the
        # redirect status is fed back to us with
        # the fetched data.

        self.server.setResponse(response, '/redirected')

        response1 = self.cache.fetch(self.TEST_URL)

        # The response should include the status code we set
        self.failUnlessEqual(response1.get('status'), response)

        # The response should include the new URL, too
        self.failUnlessEqual(response1.href, self.TEST_URL + 'redirected')

        # The response should not have been cached under either URL
        self.failIf(self.TEST_URL in self.storage)
        self.failIf(self.TEST_URL + 'redirected' in self.storage)
        return

    def test301(self):
        self._test(301)

    def test302(self):
        self._test(302)

    def test303(self):
        self._test(303)

    def test307(self):
        self._test(307)


class CachePurgeTest(CacheTestBase):

    def testPurgeAll(self):
        # Remove everything from the cache

        self.cache.fetch(self.TEST_URL)
        self.failUnless(self.storage.keys(),
                        'Have no data in the cache storage')

        self.cache.purge(None)

        self.failIf(self.storage.keys(),
                    'Still have data in the cache storage')
        return

    def testPurgeByAge(self):
        # Remove old content from the cache

        self.cache.fetch(self.TEST_URL)
        self.failUnless(self.storage.keys(),
                        'have no data in the cache storage')

        time.sleep(1)

        remains = (time.time(), copy.deepcopy(self.storage[self.TEST_URL][1]))
        self.storage['http://this.should.remain/'] = remains

        self.cache.purge(1)

        self.failUnlessEqual(self.storage.keys(),
                             ['http://this.should.remain/'])
        return


if __name__ == '__main__':
    unittest.main()
