/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.osmdroid.tileprovider.modules;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mozilla.mozstumbler.client.mapview.tiles.AbstractMapOverlay;
import org.mozilla.mozstumbler.service.core.http.IHttpUtil;
import org.mozilla.mozstumbler.service.core.http.IResponse;
import org.mozilla.mozstumbler.svclocator.ServiceLocator;
import org.mozilla.mozstumbler.svclocator.services.log.LoggerUtil;
import org.mozilla.mozstumbler.test.fixtures.FixtureLoader;
import org.mozilla.osmdroid.tileprovider.MapTile;
import org.mozilla.osmdroid.tileprovider.tilesource.BitmapTileSourceBase;
import org.mozilla.osmdroid.tileprovider.tilesource.ITileSource;
import org.mozilla.osmdroid.tileprovider.tilesource.XYTileSource;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@Config(emulateSdk = 18)
@RunWith(RobolectricTestRunner.class)
@SuppressWarnings("unchecked")
public class TileDownloaderDelegateTest {
private static final String testUrl = "http://not.a.real.url/";
private final String LOG_TAG = LoggerUtil.makeLogTag(TileDownloaderDelegateTest.class);
@Test
public void testSimpleHTTP200() throws BitmapTileSourceBase.LowMemoryException, IOException {
/*
This test case mocks out enough to get the TileDownloaderDelegate to run to completion
and return real tile data.
*/
INetworkAvailablityCheck netAvailabilityCheck = mock(INetworkAvailablityCheck.class);
TileIOFacade ioFacade = mock(TileIOFacade.class);
ITileSource mockTileSource = spy(new XYTileSource("Stumbler-BaseMap-Tiles",
null, 1, AbstractMapOverlay.MAX_ZOOM_LEVEL_OF_MAP,
AbstractMapOverlay.TILE_PIXEL_SIZE,
AbstractMapOverlay.FILE_TYPE_SUFFIX_PNG,
new String[]{testUrl}));
doReturn(testUrl).when(mockTileSource).getTileURLString((MapTile) anyObject());
MapTile tile = mock(MapTile.class);
TileDownloaderDelegate delegate = spy(new TileDownloaderDelegate(netAvailabilityCheck, ioFacade));
doReturn(false).when(delegate).networkIsUnavailable();
doReturn(false).when(delegate).urlIs404Cached(anyString());
SerializableTile sTile = getSerializableTile();
assertTrue(sTile.getTileData().length > 0);
// Clobber the IHttpUtil class so that the response is just the bytes from fixture data
IHttpUtil mockHttp = mock(IHttpUtil.class);
IResponse http200 = spy(IResponse.class);
// set the 200 status code and the content body
doReturn(200).when(http200).httpStatusCode();
doReturn(sTile.getTileData()).when(http200).bodyBytes();
// Always return the mock 200 object we just cooked up
doReturn(http200).when(mockHttp).get(anyString(),
(Map<String, String>) anyObject());
ServiceLocator.getInstance().putService(IHttpUtil.class, mockHttp);
// that tileBytes are properly saved to disk.
doReturn(sTile).when(ioFacade).saveFile(any(ITileSource.class),
any(MapTile.class),
any(byte[].class),
any(String.class));
// We should have a valid Drawable instance here
assertNotNull(delegate.downloadTile(sTile, mockTileSource, tile));
}
@Test
public void testHTTP304Caching() throws BitmapTileSourceBase.LowMemoryException, IOException {
/*
This test case verifies that an HTTP 304 will properly re-use tile data from disk.
*/
INetworkAvailablityCheck netAvailabilityCheck = mock(INetworkAvailablityCheck.class);
TileIOFacade ioFacade = mock(TileIOFacade.class);
ITileSource mockTileSource = spy(new XYTileSource("Stumbler-BaseMap-Tiles",
null, 1, AbstractMapOverlay.MAX_ZOOM_LEVEL_OF_MAP,
AbstractMapOverlay.TILE_PIXEL_SIZE,
AbstractMapOverlay.FILE_TYPE_SUFFIX_PNG,
new String[]{testUrl}));
doReturn(testUrl).when(mockTileSource).getTileURLString((MapTile) anyObject());
MapTile tile = mock(MapTile.class);
TileDownloaderDelegate delegate = spy(new TileDownloaderDelegate(netAvailabilityCheck, ioFacade));
doReturn(false).when(delegate).networkIsUnavailable();
doReturn(false).when(delegate).urlIs404Cached(anyString());
SerializableTile sTile = getSerializableTile();
String expectedEtag = sTile.getEtag();
assertTrue(sTile.getTileData().length > 0);
// Clobber the IHttpUtil class so that the response is just the bytes from fixture data
IHttpUtil mockHttp = mock(IHttpUtil.class);
IResponse http304 = spy(IResponse.class);
// set the 200 status code and the content body
doReturn(304).when(http304).httpStatusCode();
doReturn(new byte[0]).when(http304).bodyBytes();
// Always return the mock 200 object we just cooked up
ArgumentCaptor<Map<String, String>> argument = ArgumentCaptor.forClass((Class) HashMap.class);
doReturn(http304).when(mockHttp).get(anyString(), argument.capture());
ServiceLocator.getInstance().putService(IHttpUtil.class, mockHttp);
// We should have a valid Drawable instance here
assertNotNull(delegate.downloadTile(sTile, mockTileSource, tile));
// We can only check the capture *after* the delegate's downloadTile method has been called.
// verify that the etag was passed into the hashmap
assertTrue(argument.getValue().containsKey("If-None-Match"));
String actualEtag = argument.getValue().get("If-None-Match");
assertEquals(actualEtag, expectedEtag);
}
@Test
public void testHTTP304CachingCorruptTile() throws BitmapTileSourceBase.LowMemoryException, IOException {
/*
This test case verifies that an HTTP 304 will return NULL and clear the etag on the serializable
tile in the event that a conditional get succeeds, but the ondisk tile is corrupt.
*/
INetworkAvailablityCheck netAvailabilityCheck = mock(INetworkAvailablityCheck.class);
TileIOFacade ioFacade = mock(TileIOFacade.class);
ITileSource mockTileSource = spy(new XYTileSource("Stumbler-BaseMap-Tiles",
null, 1, AbstractMapOverlay.MAX_ZOOM_LEVEL_OF_MAP,
AbstractMapOverlay.TILE_PIXEL_SIZE,
AbstractMapOverlay.FILE_TYPE_SUFFIX_PNG,
new String[]{testUrl}));
doReturn(testUrl).when(mockTileSource).getTileURLString((MapTile) anyObject());
MapTile tile = mock(MapTile.class);
TileDownloaderDelegate delegate = spy(new TileDownloaderDelegate(netAvailabilityCheck, ioFacade));
doReturn(false).when(delegate).networkIsUnavailable();
doReturn(false).when(delegate).urlIs404Cached(anyString());
SerializableTile sTile = getEmptySerializableTile("66aa0fd89644ec8814559dcecbd47490");
String expectedEtag = sTile.getEtag();
assertEquals(0, sTile.getTileData().length);
// Clobber the IHttpUtil class so that the response is just the bytes from fixture data
IHttpUtil mockHttp = mock(IHttpUtil.class);
IResponse http304 = spy(IResponse.class);
// set the 200 status code and the content body
doReturn(304).when(http304).httpStatusCode();
// Return an empty array to simulate the file has been truncated.
doReturn(new byte[0]).when(http304).bodyBytes();
// Always return the mock 200 object we just cooked up
ArgumentCaptor<Map<String, String>> argument = ArgumentCaptor.forClass((Class) HashMap.class);
doReturn(http304).when(mockHttp).get(anyString(), argument.capture());
ServiceLocator.getInstance().putService(IHttpUtil.class, mockHttp);
// We should *not* have a valid Drawable instance here
assertNull(delegate.downloadTile(sTile, mockTileSource, tile));
// We can only check the capture *after* the delegate's downloadTile method has been called.
// verify that the etag was passed into the hashmap
assertTrue(argument.getValue().containsKey("If-None-Match"));
String actualEtag = argument.getValue().get("If-None-Match");
assertEquals(actualEtag, expectedEtag);
// The etag on the SerializableTile should have been cleared.
assertEquals("", sTile.getEtag());
}
@Test
public void testNetworkIsDownDiskHasTile() throws BitmapTileSourceBase.LowMemoryException, IOException {
/*
This test case mocks out enough to get the TileDownloaderDelegate to run to completion
and return real tile data.
*/
INetworkAvailablityCheck netAvailabilityCheck = mock(INetworkAvailablityCheck.class);
TileIOFacade ioFacade = mock(TileIOFacade.class);
ITileSource mockTileSource = spy(new XYTileSource("Stumbler-BaseMap-Tiles",
null, 1, AbstractMapOverlay.MAX_ZOOM_LEVEL_OF_MAP,
AbstractMapOverlay.TILE_PIXEL_SIZE,
AbstractMapOverlay.FILE_TYPE_SUFFIX_PNG,
new String[]{testUrl}));
doReturn(testUrl).when(mockTileSource).getTileURLString((MapTile) anyObject());
MapTile tile = mock(MapTile.class);
TileDownloaderDelegate delegate = spy(new TileDownloaderDelegate(netAvailabilityCheck, ioFacade));
doReturn(true).when(delegate).networkIsUnavailable();
SerializableTile sTile = getSerializableTile();
assertTrue(sTile.getTileData().length > 0);
// We should have a valid Drawable instance here
assertNotNull(delegate.downloadTile(sTile, mockTileSource, tile));
}
@Test
public void testNetworkIsDownNoData() throws BitmapTileSourceBase.LowMemoryException, IOException {
/*
This test case mocks out enough to get the TileDownloaderDelegate to run to completion
and return real tile data.
*/
INetworkAvailablityCheck netAvailabilityCheck = mock(INetworkAvailablityCheck.class);
TileIOFacade ioFacade = mock(TileIOFacade.class);
ITileSource mockTileSource = spy(new XYTileSource("Stumbler-BaseMap-Tiles",
null, 1, AbstractMapOverlay.MAX_ZOOM_LEVEL_OF_MAP,
AbstractMapOverlay.TILE_PIXEL_SIZE,
AbstractMapOverlay.FILE_TYPE_SUFFIX_PNG,
new String[]{testUrl}));
doReturn(testUrl).when(mockTileSource).getTileURLString((MapTile) anyObject());
MapTile tile = mock(MapTile.class);
TileDownloaderDelegate delegate = spy(new TileDownloaderDelegate(netAvailabilityCheck, ioFacade));
doReturn(true).when(delegate).networkIsUnavailable();
SerializableTile sTile = getEmptySerializableTile("blank_etag");
assertTrue(sTile.getTileData().length == 0);
// We should have null here
assertNull(delegate.downloadTile(sTile, mockTileSource, tile));
}
@Test
public void testHttp598SocketError() throws BitmapTileSourceBase.LowMemoryException, IOException {
INetworkAvailablityCheck netAvailabilityCheck = mock(INetworkAvailablityCheck.class);
TileIOFacade ioFacade = mock(TileIOFacade.class);
ITileSource mockTileSource = spy(new XYTileSource("Stumbler-BaseMap-Tiles",
null, 1, AbstractMapOverlay.MAX_ZOOM_LEVEL_OF_MAP,
AbstractMapOverlay.TILE_PIXEL_SIZE,
AbstractMapOverlay.FILE_TYPE_SUFFIX_PNG,
new String[]{testUrl}));
doReturn(testUrl).when(mockTileSource).getTileURLString((MapTile) anyObject());
MapTile tile = mock(MapTile.class);
TileDownloaderDelegate delegate = spy(new TileDownloaderDelegate(netAvailabilityCheck, ioFacade));
doReturn(false).when(delegate).networkIsUnavailable();
doReturn(false).when(delegate).urlIs404Cached(anyString());
SerializableTile sTile = getSerializableTile();
assertTrue(sTile.getTileData().length > 0);
// Clobber the IHttpUtil class so that the response is just the bytes from fixture data
IHttpUtil mockHttp = mock(IHttpUtil.class);
IResponse http598 = spy(IResponse.class);
// set the 598 status code and the content body
doReturn(598).when(http598).httpStatusCode();
// Always return the mock 598 object we just cooked up
doReturn(http598).when(mockHttp).get(anyString(),
(Map<String, String>) anyObject());
ServiceLocator.getInstance().putService(IHttpUtil.class, mockHttp);
// No data should have come back
assertNull(delegate.downloadTile(sTile, mockTileSource, tile));
// Make sure that the Http Client was actually called, there are multiple exit points
// in the TileDownloaderDelegate
verify(mockHttp, times(1)).get((String)anyObject(),(Map<String, String>)anyObject());
}
private SerializableTile getEmptySerializableTile(String etag) throws IOException {
// Check that we've actually downloaded the file
return new SerializableTile(new byte[0], etag);
}
private SerializableTile getSerializableTile() throws IOException {
// Check that we've actually downloaded the file
File tmpFile = File.createTempFile("stile", "tmpfile");
String absPath = tmpFile.getAbsolutePath();
FileOutputStream stream = new FileOutputStream(tmpFile);
try {
byte[] bytes = FixtureLoader.loadResource("org/mozilla/mozstumbler/test/fixtures/2976.png.merged");
assertTrue(bytes.length > 0);
stream.write(bytes);
} finally {
stream.flush();
stream.close();
}
return new SerializableTile(new File(absPath));
}
}