/* * Copyright (C) 2015 Patryk Strach * * This file is part of Virtual Slide Viewer. * * Virtual Slide Viewer is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software Foundation, * either version 3 of the License, or (at your option) any later version. * * Virtual Slide Viewer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with Virtual Slide Viewer. * If not, see <http://www.gnu.org/licenses/>. */ package virtualslideviewer.imageviewing; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.*; import java.awt.Dimension; import java.awt.Rectangle; import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import org.junit.Before; import org.junit.Test; import org.mockito.InOrder; import org.mockito.Mockito; import virtualslideviewer.core.BufferedVirtualSlideImage; import virtualslideviewer.core.ImageIndex; import virtualslideviewer.core.Tile; import virtualslideviewer.imageviewing.AsyncVisibleImageLoader; import virtualslideviewer.imageviewing.LoadingTilePlaceholderGenerator; import virtualslideviewer.imageviewing.PrefetchingStrategy; import virtualslideviewer.imageviewing.TileLoadingPrioritizer; import virtualslideviewer.imageviewing.VisibleImageLoader; import virtualslideviewer.testutils.TestUtil; public class AsyncVisibleImageLoaderTest { private ExecutorService mExecutorMock; private TileLoadingPrioritizer mLoadingPrioritizerMock; private PrefetchingStrategy mPrefetchingStrategyMock; private LoadingTilePlaceholderGenerator mPlaceholderGeneratorMock; private BufferedVirtualSlideImage mImageMock; private VisibleImageLoader mTestedLoader; @Before public void setUp() throws Exception { mExecutorMock = Mockito.mock(ExecutorService.class); mLoadingPrioritizerMock = Mockito.mock(TileLoadingPrioritizer.class); mPrefetchingStrategyMock = Mockito.mock(PrefetchingStrategy.class); mPlaceholderGeneratorMock = Mockito.mock(LoadingTilePlaceholderGenerator.class); mImageMock = TestUtil.createImageMockWithDefaultParameters(); // Execute submitted tasks synchronously Mockito.when(mExecutorMock.submit(Mockito.any())).then((x) -> { Runnable task = (Runnable)x.getArguments()[0]; task.run(); return null; }); mTestedLoader = new AsyncVisibleImageLoader(mExecutorMock, mPlaceholderGeneratorMock, mPrefetchingStrategyMock, mLoadingPrioritizerMock); } @Test(expected = IllegalArgumentException.class) public void testLoaderThrowsOnTooSmallBuffer() { mTestedLoader.getVisibleImageData(mImageMock, new byte[2000], new Rectangle(0, 0, 1000, 1500), new ImageIndex(1), () -> {}); } @Test public void testLoaderCancelsAllTasksFromPreviousCallToGetSubImageData() { Mockito.when(mImageMock.getTileSize(1)).thenReturn(new Dimension(10, 30)); Future<?>[] mockedFutures = { Mockito.mock(Future.class), Mockito.mock(Future.class), Mockito.mock(Future.class) } ; Mockito.doReturn(mockedFutures[0]) .doReturn(mockedFutures[1]) .doReturn(mockedFutures[2]).when(mExecutorMock).submit(Mockito.any()); Mockito.when(mPrefetchingStrategyMock.getTilesToPrefetch(Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(Arrays.asList(new Tile(1, 2, new ImageIndex(1)))); mTestedLoader.getVisibleImageData(mImageMock, new byte[1100], new Rectangle(20, 120, 10, 60), new ImageIndex(1), () -> {}); mTestedLoader.getVisibleImageData(mImageMock, new byte[1200], new Rectangle(20, 120, 20, 60), new ImageIndex(1), () -> {}); Mockito.verify(mockedFutures[0]).cancel(Mockito.anyBoolean()); Mockito.verify(mockedFutures[1]).cancel(Mockito.anyBoolean()); Mockito.verify(mockedFutures[2]).cancel(Mockito.anyBoolean()); } @Test public void testLoaderPrefetchesAllTilesReturnedByPrefetchingStrategy() { Mockito.when(mImageMock.getTileSize(1)).thenReturn(new Dimension(10, 30)); Rectangle regionToLoad = new Rectangle(10, 15, 25, 30); List<Tile> tilesWhichShouldBePrefetched = Arrays.asList(new Tile(10, 10, new ImageIndex(1)), new Tile(12, 15, new ImageIndex(1)), new Tile(8, 5, new ImageIndex(1))); Mockito.when(mPrefetchingStrategyMock.getTilesToPrefetch(mImageMock, regionToLoad, new ImageIndex(1))) .thenReturn(tilesWhichShouldBePrefetched); mTestedLoader.getVisibleImageData(mImageMock, new byte[1000], regionToLoad, new ImageIndex(1), () -> {}); Mockito.verify(mPrefetchingStrategyMock).getTilesToPrefetch(mImageMock, regionToLoad, new ImageIndex(1)); Mockito.verify(mImageMock).ensureTileDataCached(tilesWhichShouldBePrefetched.get(0)); Mockito.verify(mImageMock).ensureTileDataCached(tilesWhichShouldBePrefetched.get(1)); Mockito.verify(mImageMock).ensureTileDataCached(tilesWhichShouldBePrefetched.get(2)); } @Test @SuppressWarnings("unchecked") public void testLoaderLoadsTilesInOrderSpecifiedByTilePrioritizer() { Mockito.when(mImageMock.getTileSize(1)).thenReturn(new Dimension(10, 30)); Rectangle regionToLoad = new Rectangle(52, 250, 16, 30); List<Tile> expectedTileOrder = Arrays.asList(new Tile(6, 9, new ImageIndex(1)), new Tile(5, 8, new ImageIndex(1)), new Tile(6, 8, new ImageIndex(1)), new Tile(5, 9, new ImageIndex(1))); Mockito.doAnswer((x) -> { List<Tile> result = (List<Tile>)x.getArguments()[0]; result.clear(); result.addAll(expectedTileOrder); return null; }).when(mLoadingPrioritizerMock).sortTilesByPriority(Mockito.any(), Mockito.eq(mImageMock), Mockito.eq(regionToLoad)); mTestedLoader.getVisibleImageData(mImageMock, new byte[1000], regionToLoad, new ImageIndex(1), () -> {}); InOrder inOrder = Mockito.inOrder(mPlaceholderGeneratorMock); inOrder.verify(mPlaceholderGeneratorMock).getTilePlaceholder(Mockito.any(), Mockito.eq(mImageMock), Mockito.eq(expectedTileOrder.get(0))); inOrder.verify(mPlaceholderGeneratorMock).getTilePlaceholder(Mockito.any(), Mockito.eq(mImageMock), Mockito.eq(expectedTileOrder.get(1))); inOrder.verify(mPlaceholderGeneratorMock).getTilePlaceholder(Mockito.any(), Mockito.eq(mImageMock), Mockito.eq(expectedTileOrder.get(2))); inOrder.verify(mPlaceholderGeneratorMock).getTilePlaceholder(Mockito.any(), Mockito.eq(mImageMock), Mockito.eq(expectedTileOrder.get(3))); } @Test public void testLoaderDoesNotCallPlaceholderGeneratorNorThreadPoolWhenAllTilesAreInCache() { Mockito.when(mImageMock.getTileSize(1)).thenReturn(new Dimension(10, 30)); Mockito.when(mImageMock.isImageInCache(new Rectangle(50, 90, 10, 30), new ImageIndex(1))).thenReturn(true); Mockito.when(mImageMock.isImageInCache(new Rectangle(60, 90, 10, 30), new ImageIndex(1))).thenReturn(true); Mockito.when(mImageMock.isImageInCache(new Rectangle(50, 120, 10, 30), new ImageIndex(1))).thenReturn(true); Mockito.when(mImageMock.isImageInCache(new Rectangle(60, 120, 10, 30), new ImageIndex(1))).thenReturn(true); mTestedLoader.getVisibleImageData(mImageMock, new byte[1000], new Rectangle(55, 110, 13, 20), new ImageIndex(1), () -> {}); Mockito.verifyZeroInteractions(mPlaceholderGeneratorMock, mExecutorMock); } @Test public void testLoaderSubmitsTilesForLoadingWhenItIsNotInCache() { Mockito.when(mImageMock.getTileSize(1)).thenReturn(new Dimension(10, 30)); Mockito.when(mImageMock.isImageInCache(new Rectangle(80, 150, 10, 30), new ImageIndex(1))).thenReturn(true); mTestedLoader.getVisibleImageData(mImageMock, new byte[1000], new Rectangle(85, 160, 10, 40), new ImageIndex(1), () -> {}); Mockito.verify(mExecutorMock, Mockito.times(3)).submit(Mockito.any()); Mockito.verify(mImageMock).ensureTileDataCached(new Tile(8, 6, new ImageIndex(1))); Mockito.verify(mImageMock).ensureTileDataCached(new Tile(9, 5, new ImageIndex(1))); Mockito.verify(mImageMock).ensureTileDataCached(new Tile(9, 6, new ImageIndex(1))); } @Test public void testLoaderCallsCallbackAfterLoadingOnlyAndForAllLoadedTiles() { Mockito.when(mImageMock.getTileSize(1)).thenReturn(new Dimension(10, 30)); Mockito.when(mImageMock.isImageInCache(new Rectangle(80, 150, 10, 30), new ImageIndex(1))).thenReturn(true); Mockito.when(mPrefetchingStrategyMock.getTilesToPrefetch(Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(Arrays.asList(new Tile(1, 1, new ImageIndex(1)), new Tile(1, 2, new ImageIndex(1)))); Runnable callbackMock = Mockito.mock(Runnable.class); mTestedLoader.getVisibleImageData(mImageMock, new byte[1000], new Rectangle(75, 160, 12, 30), new ImageIndex(1), callbackMock); Mockito.verify(callbackMock, Mockito.times(3)).run(); } @Test public void testLoaderReadsDataFromCacheWhenIsItAvailableAndPlaceholderGeneratorIfItIsNot() { Mockito.when(mImageMock.getTileSize(1)).thenReturn(new Dimension(10, 30)); Mockito.when(mImageMock.isImageInCache(new Rectangle(80, 150, 10, 30), new ImageIndex(1))).thenReturn(true); Mockito.when(mImageMock.isImageInCache(new Rectangle(90, 180, 10, 30), new ImageIndex(1))).thenReturn(true); mTestedLoader.getVisibleImageData(mImageMock, new byte[1000], new Rectangle(85, 170, 10, 30), new ImageIndex(1), () -> {}); Mockito.verify(mPlaceholderGeneratorMock, Mockito.never()).getTilePlaceholder(Mockito.any(), Mockito.eq(mImageMock), Mockito.eq(new Tile(8, 5, new ImageIndex(1)))); Mockito.verify(mPlaceholderGeneratorMock). getTilePlaceholder(Mockito.any(), Mockito.eq(mImageMock), Mockito.eq(new Tile(8, 6, new ImageIndex(1)))); Mockito.verify(mPlaceholderGeneratorMock). getTilePlaceholder(Mockito.any(), Mockito.eq(mImageMock), Mockito.eq(new Tile(9, 5, new ImageIndex(1)))); Mockito.verify(mPlaceholderGeneratorMock, Mockito.never()).getTilePlaceholder(Mockito.any(), Mockito.eq(mImageMock), Mockito.eq(new Tile(9, 6, new ImageIndex(1)))); Mockito.verify(mImageMock). getTileData(Mockito.any(), Mockito.eq(new Tile(8, 5, new ImageIndex(1)))); Mockito.verify(mImageMock, Mockito.never()).getTileData(Mockito.any(), Mockito.eq(new Tile(8, 6, new ImageIndex(1)))); Mockito.verify(mImageMock, Mockito.never()).getTileData(Mockito.any(), Mockito.eq(new Tile(9, 5, new ImageIndex(1)))); Mockito.verify(mImageMock). getTileData(Mockito.any(), Mockito.eq(new Tile(9, 6, new ImageIndex(1)))); } @Test public void testLoaderReturnsCorrectDataWithGrayImages() { Mockito.when(mImageMock.getTileSize(0)).thenReturn(new Dimension(2, 3)); Mockito.when(mImageMock.isImageInCache(new Rectangle(14, 9, 2, 3), new ImageIndex(0))).thenReturn(true); Mockito.when(mImageMock.isImageInCache(new Rectangle(16, 12, 2, 3), new ImageIndex(0))).thenReturn(true); setImageTileDataToItsCoordsPlus100(new Tile(7, 3, new ImageIndex(0)), 2 * 3); setImageTileDataToItsCoordsPlus100(new Tile(8, 4, new ImageIndex(0)), 2 * 3); setPlaceholderTileDataToItsCoords( new Tile(6, 3, new ImageIndex(0)), 2 * 3); setPlaceholderTileDataToItsCoords( new Tile(8, 3, new ImageIndex(0)), 2 * 3); setPlaceholderTileDataToItsCoords( new Tile(6, 4, new ImageIndex(0)), 2 * 3); setPlaceholderTileDataToItsCoords( new Tile(7, 4, new ImageIndex(0)), 2 * 3); byte[] expectedResult = { 63, (byte)173, (byte)173, 83, 63, (byte)173, (byte)173, 83, 63, (byte)173, (byte)173, 83, 64, 74, 74, (byte)184, 64, 74, 74, (byte)184 }; byte[] result = new byte[4 * 5]; mTestedLoader.getVisibleImageData(mImageMock, result, new Rectangle(13, 9, 4, 5), new ImageIndex(0), () -> {}); assertThat(result, is(expectedResult)); } @Test public void testLoaderReturnsCorrectDataWithRGBImages() { Mockito.when(mImageMock.isRGB()).thenReturn(true); Mockito.when(mImageMock.getTileSize(0)).thenReturn(new Dimension(2, 3)); Mockito.when(mImageMock.isImageInCache(new Rectangle(2, 6, 2, 3), new ImageIndex(0))).thenReturn(true); Mockito.when(mImageMock.isImageInCache(new Rectangle(2, 9, 2, 3), new ImageIndex(0))).thenReturn(true); setImageTileDataToItsCoordsPlus100(new Tile(1, 2, new ImageIndex(0)), 2 * 3 * 3); setImageTileDataToItsCoordsPlus100(new Tile(1, 3, new ImageIndex(0)), 2 * 3 * 3); setPlaceholderTileDataToItsCoords( new Tile(2, 2, new ImageIndex(0)), 2 * 3 * 3); setPlaceholderTileDataToItsCoords( new Tile(2, 3, new ImageIndex(0)), 2 * 3 * 3); byte[] expectedResult = { 112, 112, 112, 112, 112, 112, 22, 22, 22, 112, 112, 112, 112, 112, 112, 22, 22, 22, 112, 112, 112, 112, 112, 112, 22, 22, 22, 113, 113, 113, 113, 113, 113, 23, 23, 23 }; byte[] result = new byte[3 * 4 * 3]; mTestedLoader.getVisibleImageData(mImageMock, result, new Rectangle(2, 6, 3, 4), new ImageIndex(0), () -> {}); assertThat(result, is(expectedResult)); } /** * Sets data returned from mImageMock.getTileData() to tile's coordinates + 100, i. e. * tile with coordinates (4, 2) will have all its bytes set to 142, tile (1, 5) to 115 and so on. */ private void setImageTileDataToItsCoordsPlus100(Tile tile, int tileDataSize) { byte[] tileData = new byte[tileDataSize]; for(int i = 0; i < tileDataSize; i++) { tileData[i] = (byte)(100 + tile.getColumn() * 10 + tile.getRow()); } TestUtil.copyToParameter(tileData).when(mImageMock).getTileData(Mockito.any(), Mockito.eq(tile)); } /** * Sets data returned from mPlaceholderGeneratorMock.getTilePlaceholder() to tile's coordinates, i. e. * tile with coordinates (4, 2) will have all its bytes set to 42, tile (1, 5) to 15 and so on. */ private void setPlaceholderTileDataToItsCoords(Tile tile, int tileDataSize) { byte[] tileData = new byte[tileDataSize]; for(int i = 0; i < tileDataSize; i++) { tileData[i] = (byte)(tile.getColumn() * 10 + tile.getRow()); } TestUtil.copyToParameter(tileData).when(mPlaceholderGeneratorMock).getTilePlaceholder(Mockito.any(), Mockito.any(), Mockito.eq(tile)); } }