Local feeds: Unit tests for LocalFeedUpdater (#4551)
This commit is contained in:
parent
41580b57cc
commit
28ebbedbdf
|
@ -52,6 +52,12 @@ android {
|
|||
dimension "market"
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -94,7 +100,9 @@ dependencies {
|
|||
|
||||
testImplementation "org.awaitility:awaitility:$awaitilityVersion"
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
testImplementation 'org.mockito:mockito-inline:3.5.13'
|
||||
testImplementation 'org.robolectric:robolectric:4.3.1'
|
||||
testImplementation 'javax.inject:javax.inject:1'
|
||||
androidTestImplementation "com.jayway.android.robotium:robotium-solo:$robotiumSoloVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
|
||||
androidTestImplementation "androidx.test:runner:$runnerVersion"
|
||||
|
|
|
@ -358,6 +358,21 @@ public class PodDBAdapter {
|
|||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Resets all database connections to ensure new database connections for
|
||||
* the next test case. Call method only for unit tests.</p>
|
||||
*
|
||||
* <p>That's a workaround for a Robolectric issue in ShadowSQLiteConnection
|
||||
* that leads to an error <tt>IllegalStateException: Illegal connection
|
||||
* pointer</tt> if several threads try to use the same database connection.
|
||||
* For more information see
|
||||
* <a href="https://github.com/robolectric/robolectric/issues/1890">robolectric/robolectric#1890</a>.</p>
|
||||
*/
|
||||
public static void tearDownTests() {
|
||||
db = null;
|
||||
SingletonHolder.dbHelper.close();
|
||||
}
|
||||
|
||||
public static boolean deleteDatabase() {
|
||||
PodDBAdapter adapter = getInstance();
|
||||
adapter.open();
|
||||
|
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,138 @@
|
|||
package androidx.documentfile.provider;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.net.Uri;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* <p>Wraps an Android assets file or folder as a DocumentFile object.</p>
|
||||
*
|
||||
* <p>This is used to emulate access to the external storage.</p>
|
||||
*/
|
||||
public class AssetsDocumentFile extends DocumentFile {
|
||||
|
||||
/**
|
||||
* Absolute file path in the assets folder.
|
||||
*/
|
||||
@NonNull
|
||||
private final String fileName;
|
||||
|
||||
@NonNull
|
||||
private final AssetManager assetManager;
|
||||
|
||||
public AssetsDocumentFile(@NonNull String fileName, @NonNull AssetManager assetManager) {
|
||||
super(null);
|
||||
this.fileName = fileName;
|
||||
this.assetManager = assetManager;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DocumentFile createFile(@NonNull String mimeType, @NonNull String displayName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DocumentFile createDirectory(@NonNull String displayName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Uri getUri() {
|
||||
return Uri.parse(fileName);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getName() {
|
||||
int pos = fileName.indexOf('/');
|
||||
if (pos >= 0) {
|
||||
return fileName.substring(pos + 1);
|
||||
} else {
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getType() {
|
||||
String extension = MimeTypeMap.getFileExtensionFromUrl(fileName);
|
||||
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFile() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVirtual() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long lastModified() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long length() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DocumentFile[] listFiles() {
|
||||
try {
|
||||
String[] files = assetManager.list(fileName);
|
||||
if (files == null) {
|
||||
return new DocumentFile[0];
|
||||
}
|
||||
DocumentFile[] result = new DocumentFile[files.length];
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
String subFileName = fileName + '/' + files[i];
|
||||
result[i] = new AssetsDocumentFile(subFileName, assetManager);
|
||||
}
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
return new DocumentFile[0];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean renameTo(@NonNull String displayName) {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
package de.danoeh.antennapod.core.feed;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.documentfile.provider.AssetsDocumentFile;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.shadows.ShadowMediaMetadataRetriever;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import de.danoeh.antennapod.core.ApplicationCallbacks;
|
||||
import de.danoeh.antennapod.core.ClientConfig;
|
||||
import de.danoeh.antennapod.core.R;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.storage.DBReader;
|
||||
import de.danoeh.antennapod.core.storage.PodDBAdapter;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
/**
|
||||
* Test local feeds handling in class LocalFeedUpdater.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class LocalFeedUpdaterTest {
|
||||
|
||||
/**
|
||||
* URL to locate the local feed media files on the external storage (SD card).
|
||||
* The exact URL doesn't matter here as access to external storage is mocked
|
||||
* (seems not to be supported by Robolectric).
|
||||
*/
|
||||
private static final String FEED_URL =
|
||||
"content://com.android.externalstorage.documents/tree/primary%3ADownload%2Flocal-feed";
|
||||
private static final String LOCAL_FEED_DIR1 = "local-feed1";
|
||||
private static final String LOCAL_FEED_DIR2 = "local-feed2";
|
||||
|
||||
private Context context;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
// Initialize environment
|
||||
context = InstrumentationRegistry.getInstrumentation().getContext();
|
||||
UserPreferences.init(context);
|
||||
|
||||
Application app = (Application) context;
|
||||
ClientConfig.applicationCallbacks = mock(ApplicationCallbacks.class);
|
||||
when(ClientConfig.applicationCallbacks.getApplicationInstance()).thenReturn(app);
|
||||
|
||||
// Initialize database
|
||||
PodDBAdapter.init(context);
|
||||
PodDBAdapter.deleteDatabase();
|
||||
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||
adapter.open();
|
||||
adapter.close();
|
||||
|
||||
mapDummyMetadata(LOCAL_FEED_DIR1);
|
||||
mapDummyMetadata(LOCAL_FEED_DIR2);
|
||||
shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("mp3", "audio/mp3");
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
PodDBAdapter.tearDownTests();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test adding a new local feed.
|
||||
*/
|
||||
@Test
|
||||
public void testUpdateFeed_AddNewFeed() {
|
||||
// check for empty database
|
||||
List<Feed> feedListBefore = DBReader.getFeedList();
|
||||
assertTrue(feedListBefore.isEmpty());
|
||||
|
||||
callUpdateFeed(LOCAL_FEED_DIR2);
|
||||
|
||||
// verify new feed in database
|
||||
verifySingleFeedInDatabaseAndItemCount(2);
|
||||
Feed feedAfter = verifySingleFeedInDatabase();
|
||||
assertEquals(FEED_URL, feedAfter.getDownload_url());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test adding further items to an existing local feed.
|
||||
*/
|
||||
@Test
|
||||
public void testUpdateFeed_AddMoreItems() {
|
||||
// add local feed with 1 item (localFeedDir1)
|
||||
callUpdateFeed(LOCAL_FEED_DIR1);
|
||||
|
||||
// now add another item (by changing to local feed folder localFeedDir2)
|
||||
callUpdateFeed(LOCAL_FEED_DIR2);
|
||||
|
||||
verifySingleFeedInDatabaseAndItemCount(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test removing items from an existing local feed without a corresponding media file.
|
||||
*/
|
||||
@Test
|
||||
public void testUpdateFeed_RemoveItems() {
|
||||
// add local feed with 2 items (localFeedDir1)
|
||||
callUpdateFeed(LOCAL_FEED_DIR2);
|
||||
|
||||
// now remove an item (by changing to local feed folder localFeedDir1)
|
||||
callUpdateFeed(LOCAL_FEED_DIR1);
|
||||
|
||||
verifySingleFeedInDatabaseAndItemCount(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test feed icon defined in the local feed media folder.
|
||||
*/
|
||||
@Test
|
||||
public void testUpdateFeed_FeedIconFromFolder() {
|
||||
callUpdateFeed(LOCAL_FEED_DIR2);
|
||||
|
||||
Feed feedAfter = verifySingleFeedInDatabase();
|
||||
assertTrue(feedAfter.getImageUrl().contains("local-feed2/folder.png"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test default feed icon if there is no matching file in the local feed media folder.
|
||||
*/
|
||||
@Test
|
||||
public void testUpdateFeed_FeedIconDefault() {
|
||||
callUpdateFeed(LOCAL_FEED_DIR1);
|
||||
|
||||
Feed feedAfter = verifySingleFeedInDatabase();
|
||||
String resourceEntryName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
|
||||
assertTrue(feedAfter.getImageUrl().contains(resourceEntryName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill ShadowMediaMetadataRetriever with dummy duration and title.
|
||||
*
|
||||
* @param localFeedDir assets local feed folder with media files
|
||||
*/
|
||||
private void mapDummyMetadata(@NonNull String localFeedDir) throws IOException {
|
||||
String[] fileNames = context.getAssets().list(localFeedDir);
|
||||
for (String fileName : fileNames) {
|
||||
String path = localFeedDir + '/' + fileName;
|
||||
ShadowMediaMetadataRetriever.addMetadata(path,
|
||||
MediaMetadataRetriever.METADATA_KEY_DURATION, "10");
|
||||
ShadowMediaMetadataRetriever.addMetadata(path,
|
||||
MediaMetadataRetriever.METADATA_KEY_TITLE, fileName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the method {@link LocalFeedUpdater#updateFeed(Feed, Context)} with
|
||||
* the given local feed folder.
|
||||
*
|
||||
* @param localFeedDir assets local feed folder with media files
|
||||
*/
|
||||
private void callUpdateFeed(@NonNull String localFeedDir) {
|
||||
DocumentFile documentFile = new AssetsDocumentFile(localFeedDir, context.getAssets());
|
||||
try (MockedStatic<DocumentFile> dfMock = Mockito.mockStatic(DocumentFile.class)) {
|
||||
// mock external storage
|
||||
dfMock.when(() -> DocumentFile.fromTreeUri(any(), any())).thenReturn(documentFile);
|
||||
|
||||
// call method to test
|
||||
Feed feed = new Feed(FEED_URL, null);
|
||||
LocalFeedUpdater.updateFeed(feed, context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the database contains exactly one feed and return that feed.
|
||||
*/
|
||||
@NonNull
|
||||
private static Feed verifySingleFeedInDatabase() {
|
||||
List<Feed> feedListAfter = DBReader.getFeedList();
|
||||
assertEquals(1, feedListAfter.size());
|
||||
return feedListAfter.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the database contains exactly one feed and the number of
|
||||
* items in the feed.
|
||||
*
|
||||
* @param expectedItemCount expected number of items in the feed
|
||||
*/
|
||||
private static void verifySingleFeedInDatabaseAndItemCount(int expectedItemCount) {
|
||||
Feed feed = verifySingleFeedInDatabase();
|
||||
List<FeedItem> feedItems = DBReader.getFeedItemList(feed);
|
||||
assertEquals(expectedItemCount, feedItems.size());
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ import static java.util.Collections.emptyList;
|
|||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.stub;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ItemEnqueuePositionCalculatorTest {
|
||||
|
||||
|
@ -189,7 +189,7 @@ public class ItemEnqueuePositionCalculatorTest {
|
|||
//
|
||||
ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options);
|
||||
DownloadStateProvider stubDownloadStateProvider = mock(DownloadStateProvider.class);
|
||||
stub(stubDownloadStateProvider.isDownloadingFile(any(FeedMedia.class))).toReturn(false);
|
||||
when(stubDownloadStateProvider.isDownloadingFile(any(FeedMedia.class))).thenReturn(false);
|
||||
calculator.downloadStateProvider = stubDownloadStateProvider;
|
||||
|
||||
// Setup initial data
|
||||
|
@ -232,7 +232,7 @@ public class ItemEnqueuePositionCalculatorTest {
|
|||
|
||||
private static FeedItem setAsDownloading(FeedItem item, DownloadStateProvider stubDownloadStateProvider,
|
||||
boolean isDownloading) {
|
||||
stub(stubDownloadStateProvider.isDownloadingFile(item.getMedia())).toReturn(isDownloading);
|
||||
when(stubDownloadStateProvider.isDownloadingFile(item.getMedia())).thenReturn(isDownloading);
|
||||
return item;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue