Local feeds: Unit tests for LocalFeedUpdater (#4551)
This commit is contained in:
parent
41580b57cc
commit
28ebbedbdf
@ -52,6 +52,12 @@ android {
|
|||||||
dimension "market"
|
dimension "market"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
includeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@ -94,7 +100,9 @@ dependencies {
|
|||||||
|
|
||||||
testImplementation "org.awaitility:awaitility:$awaitilityVersion"
|
testImplementation "org.awaitility:awaitility:$awaitilityVersion"
|
||||||
testImplementation 'junit:junit:4.13'
|
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 "com.jayway.android.robotium:robotium-solo:$robotiumSoloVersion"
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
|
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
|
||||||
androidTestImplementation "androidx.test:runner:$runnerVersion"
|
androidTestImplementation "androidx.test:runner:$runnerVersion"
|
||||||
|
@ -358,6 +358,21 @@ public class PodDBAdapter {
|
|||||||
// do nothing
|
// 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() {
|
public static boolean deleteDatabase() {
|
||||||
PodDBAdapter adapter = getInstance();
|
PodDBAdapter adapter = getInstance();
|
||||||
adapter.open();
|
adapter.open();
|
||||||
|
BIN
core/src/test/assets/local-feed1/track1.mp3
Normal file
BIN
core/src/test/assets/local-feed1/track1.mp3
Normal file
Binary file not shown.
BIN
core/src/test/assets/local-feed2/folder.png
Normal file
BIN
core/src/test/assets/local-feed2/folder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
core/src/test/assets/local-feed2/track1.mp3
Normal file
BIN
core/src/test/assets/local-feed2/track1.mp3
Normal file
Binary file not shown.
BIN
core/src/test/assets/local-feed2/track2.mp3
Normal file
BIN
core/src/test/assets/local-feed2/track2.mp3
Normal file
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.junit.Assert.assertEquals;
|
||||||
import static org.mockito.Mockito.any;
|
import static org.mockito.Mockito.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.stub;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class ItemEnqueuePositionCalculatorTest {
|
public class ItemEnqueuePositionCalculatorTest {
|
||||||
|
|
||||||
@ -189,7 +189,7 @@ public class ItemEnqueuePositionCalculatorTest {
|
|||||||
//
|
//
|
||||||
ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options);
|
ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options);
|
||||||
DownloadStateProvider stubDownloadStateProvider = mock(DownloadStateProvider.class);
|
DownloadStateProvider stubDownloadStateProvider = mock(DownloadStateProvider.class);
|
||||||
stub(stubDownloadStateProvider.isDownloadingFile(any(FeedMedia.class))).toReturn(false);
|
when(stubDownloadStateProvider.isDownloadingFile(any(FeedMedia.class))).thenReturn(false);
|
||||||
calculator.downloadStateProvider = stubDownloadStateProvider;
|
calculator.downloadStateProvider = stubDownloadStateProvider;
|
||||||
|
|
||||||
// Setup initial data
|
// Setup initial data
|
||||||
@ -232,7 +232,7 @@ public class ItemEnqueuePositionCalculatorTest {
|
|||||||
|
|
||||||
private static FeedItem setAsDownloading(FeedItem item, DownloadStateProvider stubDownloadStateProvider,
|
private static FeedItem setAsDownloading(FeedItem item, DownloadStateProvider stubDownloadStateProvider,
|
||||||
boolean isDownloading) {
|
boolean isDownloading) {
|
||||||
stub(stubDownloadStateProvider.isDownloadingFile(item.getMedia())).toReturn(isDownloading);
|
when(stubDownloadStateProvider.isDownloadingFile(item.getMedia())).thenReturn(isDownloading);
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user