зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1215106 - DownloadContentService: Mark content as permanently failed after multiple errors of the same type. r=rnewman
--HG-- extra : commitid : AuFevajQBmB extra : rebase_source : 55dc093df961d8e7962d3c6689a7666a924660e6
This commit is contained in:
Родитель
2b176f434e
Коммит
7943c8757e
|
@ -6,6 +6,7 @@
|
||||||
package org.mozilla.gecko.dlc;
|
package org.mozilla.gecko.dlc;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.support.annotation.IntDef;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.mozilla.gecko.background.nativecode.NativeCrypto;
|
import org.mozilla.gecko.background.nativecode.NativeCrypto;
|
||||||
|
@ -29,12 +30,40 @@ public abstract class BaseAction {
|
||||||
/* package-private */ static class RecoverableDownloadContentException extends Exception {
|
/* package-private */ static class RecoverableDownloadContentException extends Exception {
|
||||||
private static final long serialVersionUID = -2246772819507370734L;
|
private static final long serialVersionUID = -2246772819507370734L;
|
||||||
|
|
||||||
public RecoverableDownloadContentException(String message) {
|
@IntDef({MEMORY, DISK_IO, SERVER, NETWORK})
|
||||||
|
public @interface ErrorType {}
|
||||||
|
public static final int MEMORY = 1;
|
||||||
|
public static final int DISK_IO = 2;
|
||||||
|
public static final int SERVER = 3;
|
||||||
|
public static final int NETWORK = 4;
|
||||||
|
|
||||||
|
private int errorType;
|
||||||
|
|
||||||
|
public RecoverableDownloadContentException(@ErrorType int errorType, String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
this.errorType = errorType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecoverableDownloadContentException(Throwable cause) {
|
public RecoverableDownloadContentException(@ErrorType int errorType, Throwable cause) {
|
||||||
super(cause);
|
super(cause);
|
||||||
|
this.errorType = errorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ErrorType
|
||||||
|
public int getErrorType() {
|
||||||
|
return errorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should this error be counted as failure? If this type of error will happen multiple times in a row then this
|
||||||
|
* error will be treated as permanently and the operation will not be tried again until the content changes.
|
||||||
|
*/
|
||||||
|
public boolean shouldBeCountedAsFailure() {
|
||||||
|
if (NETWORK == errorType) {
|
||||||
|
return false; // Always retry after network errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +103,8 @@ public abstract class BaseAction {
|
||||||
|
|
||||||
byte[] ctx = NativeCrypto.sha256init();
|
byte[] ctx = NativeCrypto.sha256init();
|
||||||
if (ctx == null) {
|
if (ctx == null) {
|
||||||
throw new RecoverableDownloadContentException("Could not create SHA-256 context");
|
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.MEMORY,
|
||||||
|
"Could not create SHA-256 context");
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] buffer = new byte[4096];
|
byte[] buffer = new byte[4096];
|
||||||
|
@ -94,7 +124,7 @@ public abstract class BaseAction {
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Recoverable: Just I/O discontinuation
|
// Recoverable: Just I/O discontinuation
|
||||||
throw new RecoverableDownloadContentException(e);
|
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e);
|
||||||
} finally {
|
} finally {
|
||||||
IOUtils.safeStreamClose(inputStream);
|
IOUtils.safeStreamClose(inputStream);
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,6 +126,11 @@ public class DownloadAction extends BaseAction {
|
||||||
}
|
}
|
||||||
} catch (RecoverableDownloadContentException e) {
|
} catch (RecoverableDownloadContentException e) {
|
||||||
Log.w(LOGTAG, "Downloading content failed (Recoverable): " + content , e);
|
Log.w(LOGTAG, "Downloading content failed (Recoverable): " + content , e);
|
||||||
|
|
||||||
|
if (e.shouldBeCountedAsFailure()) {
|
||||||
|
catalog.rememberFailure(content, e.getErrorType());
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Reschedule download (bug 1209498)
|
// TODO: Reschedule download (bug 1209498)
|
||||||
} catch (UnrecoverableDownloadContentException e) {
|
} catch (UnrecoverableDownloadContentException e) {
|
||||||
Log.w(LOGTAG, "Downloading content failed (Unrecoverable): " + content, e);
|
Log.w(LOGTAG, "Downloading content failed (Unrecoverable): " + content, e);
|
||||||
|
@ -161,7 +166,8 @@ public class DownloadAction extends BaseAction {
|
||||||
// TODO: This is guesstimating at best. We want to implement failure counters (Bug 1215106).
|
// TODO: This is guesstimating at best. We want to implement failure counters (Bug 1215106).
|
||||||
if (status >= 500) {
|
if (status >= 500) {
|
||||||
// Recoverable: Server errors 5xx
|
// Recoverable: Server errors 5xx
|
||||||
throw new RecoverableDownloadContentException("(Recoverable) Download failed. Status code: " + status);
|
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.SERVER,
|
||||||
|
"(Recoverable) Download failed. Status code: " + status);
|
||||||
} else if (status >= 400) {
|
} else if (status >= 400) {
|
||||||
// Unrecoverable: Client errors 4xx - Unlikely that this version of the client will ever succeed.
|
// Unrecoverable: Client errors 4xx - Unlikely that this version of the client will ever succeed.
|
||||||
throw new UnrecoverableDownloadContentException("(Unrecoverable) Download failed. Status code: " + status);
|
throw new UnrecoverableDownloadContentException("(Unrecoverable) Download failed. Status code: " + status);
|
||||||
|
@ -176,7 +182,7 @@ public class DownloadAction extends BaseAction {
|
||||||
final HttpEntity entity = response.getEntity();
|
final HttpEntity entity = response.getEntity();
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
// Recoverable: Should not happen for a valid asset
|
// Recoverable: Should not happen for a valid asset
|
||||||
throw new RecoverableDownloadContentException("Null entity");
|
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.SERVER, "Null entity");
|
||||||
}
|
}
|
||||||
|
|
||||||
inputStream = new BufferedInputStream(entity.getContent());
|
inputStream = new BufferedInputStream(entity.getContent());
|
||||||
|
@ -188,7 +194,7 @@ public class DownloadAction extends BaseAction {
|
||||||
outputStream.close();
|
outputStream.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Recoverable: Just I/O discontinuation
|
// Recoverable: Just I/O discontinuation
|
||||||
throw new RecoverableDownloadContentException(e);
|
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.NETWORK, e);
|
||||||
} finally {
|
} finally {
|
||||||
IOUtils.safeStreamClose(inputStream);
|
IOUtils.safeStreamClose(inputStream);
|
||||||
IOUtils.safeStreamClose(outputStream);
|
IOUtils.safeStreamClose(outputStream);
|
||||||
|
@ -229,7 +235,7 @@ public class DownloadAction extends BaseAction {
|
||||||
move(temporaryFile, destinationFile);
|
move(temporaryFile, destinationFile);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// We could not extract to the destination: Keep temporary file and try again next time we run.
|
// We could not extract to the destination: Keep temporary file and try again next time we run.
|
||||||
throw new RecoverableDownloadContentException(e);
|
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e);
|
||||||
} finally {
|
} finally {
|
||||||
IOUtils.safeStreamClose(inputStream);
|
IOUtils.safeStreamClose(inputStream);
|
||||||
IOUtils.safeStreamClose(outputStream);
|
IOUtils.safeStreamClose(outputStream);
|
||||||
|
@ -272,7 +278,8 @@ public class DownloadAction extends BaseAction {
|
||||||
|
|
||||||
if (!cacheDirectory.exists() && !cacheDirectory.mkdirs()) {
|
if (!cacheDirectory.exists() && !cacheDirectory.mkdirs()) {
|
||||||
// Recoverable: File system might not be mounted NOW and we didn't download anything yet anyways.
|
// Recoverable: File system might not be mounted NOW and we didn't download anything yet anyways.
|
||||||
throw new RecoverableDownloadContentException("Could not create cache directory: " + cacheDirectory);
|
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO,
|
||||||
|
"Could not create cache directory: " + cacheDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new File(cacheDirectory, content.getDownloadChecksum() + "-" + content.getId());
|
return new File(cacheDirectory, content.getDownloadChecksum() + "-" + content.getId());
|
||||||
|
@ -308,7 +315,7 @@ public class DownloadAction extends BaseAction {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// We could not copy the temporary file to its destination: Keep the temporary file and
|
// We could not copy the temporary file to its destination: Keep the temporary file and
|
||||||
// try again the next time we run.
|
// try again the next time we run.
|
||||||
throw new RecoverableDownloadContentException(e);
|
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e);
|
||||||
} finally {
|
} finally {
|
||||||
IOUtils.safeStreamClose(inputStream);
|
IOUtils.safeStreamClose(inputStream);
|
||||||
IOUtils.safeStreamClose(outputStream);
|
IOUtils.safeStreamClose(outputStream);
|
||||||
|
|
|
@ -23,6 +23,8 @@ public class DownloadContent {
|
||||||
private static final String KEY_KIND = "kind";
|
private static final String KEY_KIND = "kind";
|
||||||
private static final String KEY_SIZE = "size";
|
private static final String KEY_SIZE = "size";
|
||||||
private static final String KEY_STATE = "state";
|
private static final String KEY_STATE = "state";
|
||||||
|
private static final String KEY_FAILURES = "failures";
|
||||||
|
private static final String KEY_LAST_FAILURE_TYPE = "last_failure_type";
|
||||||
|
|
||||||
@IntDef({STATE_NONE, STATE_SCHEDULED, STATE_DOWNLOADED, STATE_FAILED, STATE_IGNORED})
|
@IntDef({STATE_NONE, STATE_SCHEDULED, STATE_DOWNLOADED, STATE_FAILED, STATE_IGNORED})
|
||||||
public @interface State {}
|
public @interface State {}
|
||||||
|
@ -50,6 +52,8 @@ public class DownloadContent {
|
||||||
private final String kind;
|
private final String kind;
|
||||||
private final long size;
|
private final long size;
|
||||||
private int state;
|
private int state;
|
||||||
|
private int failures;
|
||||||
|
private int lastFailureType;
|
||||||
|
|
||||||
private DownloadContent(@NonNull String id, @NonNull String location, @NonNull String filename,
|
private DownloadContent(@NonNull String id, @NonNull String location, @NonNull String filename,
|
||||||
@NonNull String checksum, @NonNull String downloadChecksum, @NonNull long lastModified,
|
@NonNull String checksum, @NonNull String downloadChecksum, @NonNull long lastModified,
|
||||||
|
@ -121,6 +125,24 @@ public class DownloadContent {
|
||||||
return TYPE_ASSET_ARCHIVE.equals(type);
|
return TYPE_ASSET_ARCHIVE.equals(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* package-private */ int getFailures() {
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package-private */ void rememberFailure(int failureType) {
|
||||||
|
if (lastFailureType != failureType) {
|
||||||
|
lastFailureType = failureType;
|
||||||
|
failures = 1;
|
||||||
|
} else {
|
||||||
|
failures++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package-private */ void resetFailures() {
|
||||||
|
failures = 0;
|
||||||
|
lastFailureType = 0;
|
||||||
|
}
|
||||||
|
|
||||||
public static DownloadContent fromJSON(JSONObject object) throws JSONException {
|
public static DownloadContent fromJSON(JSONObject object) throws JSONException {
|
||||||
return new Builder()
|
return new Builder()
|
||||||
.setId(object.getString(KEY_ID))
|
.setId(object.getString(KEY_ID))
|
||||||
|
@ -133,6 +155,7 @@ public class DownloadContent {
|
||||||
.setKind(object.getString(KEY_KIND))
|
.setKind(object.getString(KEY_KIND))
|
||||||
.setSize(object.getLong(KEY_SIZE))
|
.setSize(object.getLong(KEY_SIZE))
|
||||||
.setState(object.getInt(KEY_STATE))
|
.setState(object.getInt(KEY_STATE))
|
||||||
|
.setFailures(object.optInt(KEY_FAILURES), object.optInt(KEY_LAST_FAILURE_TYPE))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,6 +171,12 @@ public class DownloadContent {
|
||||||
object.put(KEY_KIND, kind);
|
object.put(KEY_KIND, kind);
|
||||||
object.put(KEY_SIZE, size);
|
object.put(KEY_SIZE, size);
|
||||||
object.put(KEY_STATE, state);
|
object.put(KEY_STATE, state);
|
||||||
|
|
||||||
|
if (failures > 0) {
|
||||||
|
object.put(KEY_FAILURES, failures);
|
||||||
|
object.put(KEY_LAST_FAILURE_TYPE, lastFailureType);
|
||||||
|
}
|
||||||
|
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,11 +195,16 @@ public class DownloadContent {
|
||||||
private String kind;
|
private String kind;
|
||||||
private long size;
|
private long size;
|
||||||
private int state;
|
private int state;
|
||||||
|
private int failures;
|
||||||
|
private int lastFailureType;
|
||||||
|
|
||||||
public DownloadContent build() {
|
public DownloadContent build() {
|
||||||
DownloadContent content = new DownloadContent(id, location, filename, checksum, downloadChecksum,
|
DownloadContent content = new DownloadContent(id, location, filename, checksum, downloadChecksum,
|
||||||
lastModified, type, kind, size);
|
lastModified, type, kind, size);
|
||||||
content.setState(state);
|
content.setState(state);
|
||||||
|
content.failures = failures;
|
||||||
|
content.lastFailureType = lastFailureType;
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,5 +257,12 @@ public class DownloadContent {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* package-private */ Builder setFailures(int failures, int lastFailureType) {
|
||||||
|
this.failures = failures;
|
||||||
|
this.lastFailureType = lastFailureType;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,8 @@ public class DownloadContentCatalog {
|
||||||
private static final String LOGTAG = "GeckoDLCCatalog";
|
private static final String LOGTAG = "GeckoDLCCatalog";
|
||||||
private static final String FILE_NAME = "download_content_catalog";
|
private static final String FILE_NAME = "download_content_catalog";
|
||||||
|
|
||||||
|
private static final int MAX_FAILURES_UNTIL_PERMANENTLY_FAILED = 10;
|
||||||
|
|
||||||
private final AtomicFile file; // Guarded by 'file'
|
private final AtomicFile file; // Guarded by 'file'
|
||||||
private List<DownloadContent> content; // Guarded by 'this'
|
private List<DownloadContent> content; // Guarded by 'this'
|
||||||
private boolean hasLoadedCatalog; // Guarded by 'this
|
private boolean hasLoadedCatalog; // Guarded by 'this
|
||||||
|
@ -107,6 +109,7 @@ public class DownloadContentCatalog {
|
||||||
|
|
||||||
public synchronized void markAsDownloaded(DownloadContent content) {
|
public synchronized void markAsDownloaded(DownloadContent content) {
|
||||||
content.setState(DownloadContent.STATE_DOWNLOADED);
|
content.setState(DownloadContent.STATE_DOWNLOADED);
|
||||||
|
content.resetFailures();
|
||||||
hasCatalogChanged = true;
|
hasCatalogChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +123,17 @@ public class DownloadContentCatalog {
|
||||||
hasCatalogChanged = true;
|
hasCatalogChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void rememberFailure(DownloadContent content, int failureType) {
|
||||||
|
if (content.getFailures() >= MAX_FAILURES_UNTIL_PERMANENTLY_FAILED) {
|
||||||
|
Log.d(LOGTAG, "Maximum number of failures reached. Marking content has permanently failed.");
|
||||||
|
|
||||||
|
markAsPermanentlyFailed(content);
|
||||||
|
} else {
|
||||||
|
content.rememberFailure(failureType);
|
||||||
|
hasCatalogChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void persistChanges() {
|
public void persistChanges() {
|
||||||
new Thread(LOGTAG + "-Persist") {
|
new Thread(LOGTAG + "-Persist") {
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
|
@ -426,6 +426,83 @@ public class TestDownloadAction {
|
||||||
Assert.assertTrue(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile));
|
Assert.assertTrue(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scenario: Download failed with network I/O error.
|
||||||
|
*
|
||||||
|
* Verify that:
|
||||||
|
* * Error is not counted as failure
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testNetworkErrorIsNotCountedAsFailure() throws Exception {
|
||||||
|
DownloadContent content = createFont();
|
||||||
|
DownloadContentCatalog catalog = mockCatalogWithScheduledDownloads(content);
|
||||||
|
|
||||||
|
DownloadAction action = spy(new DownloadAction(null));
|
||||||
|
doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
|
||||||
|
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
|
||||||
|
doReturn(mockNotExistingFile()).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
|
||||||
|
doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||||
|
doReturn(true).when(action).hasEnoughDiskSpace(eq(content), any(File.class), any(File.class));
|
||||||
|
|
||||||
|
HttpClient client = mock(HttpClient.class);
|
||||||
|
doThrow(IOException.class).when(client).execute(any(HttpUriRequest.class));
|
||||||
|
doReturn(client).when(action).buildHttpClient();
|
||||||
|
|
||||||
|
action.perform(RuntimeEnvironment.application, catalog);
|
||||||
|
|
||||||
|
verify(catalog, never()).rememberFailure(eq(content), anyInt());
|
||||||
|
verify(catalog, never()).markAsDownloaded(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scenario: Disk IO Error when extracting file.
|
||||||
|
*
|
||||||
|
* Verify that:
|
||||||
|
* * Error is counted as failure
|
||||||
|
* * After multiple errors the content is marked as permanently failed
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testDiskIOErrorIsCountedAsFailure() throws Exception {
|
||||||
|
DownloadContent content = createFont();
|
||||||
|
DownloadContentCatalog catalog = mockCatalogWithScheduledDownloads(content);
|
||||||
|
doCallRealMethod().when(catalog).rememberFailure(eq(content), anyInt());
|
||||||
|
doCallRealMethod().when(catalog).markAsPermanentlyFailed(content);
|
||||||
|
|
||||||
|
Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
|
||||||
|
|
||||||
|
DownloadAction action = spy(new DownloadAction(null));
|
||||||
|
doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
|
||||||
|
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
|
||||||
|
doReturn(mockNotExistingFile()).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
|
||||||
|
doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||||
|
doReturn(true).when(action).hasEnoughDiskSpace(eq(content), any(File.class), any(File.class));
|
||||||
|
doNothing().when(action).download(any(HttpClient.class), anyString(), any(File.class));
|
||||||
|
doReturn(true).when(action).verify(any(File.class), anyString());
|
||||||
|
|
||||||
|
File destinationFile = mock(File.class);
|
||||||
|
doReturn(false).when(destinationFile).exists();
|
||||||
|
File parentFile = mock(File.class);
|
||||||
|
doReturn(false).when(parentFile).mkdirs();
|
||||||
|
doReturn(false).when(parentFile).exists();
|
||||||
|
doReturn(parentFile).when(destinationFile).getParentFile();
|
||||||
|
doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
action.perform(RuntimeEnvironment.application, catalog);
|
||||||
|
|
||||||
|
Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
action.perform(RuntimeEnvironment.application, catalog);
|
||||||
|
|
||||||
|
Assert.assertEquals(DownloadContent.STATE_FAILED, content.getState());
|
||||||
|
verify(catalog, times(11)).rememberFailure(eq(content), anyInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
private DownloadContent createFont() {
|
||||||
|
return createFontWithSize(102400L);
|
||||||
|
}
|
||||||
|
|
||||||
private DownloadContent createFontWithSize(long size) {
|
private DownloadContent createFontWithSize(long size) {
|
||||||
return new DownloadContent.Builder()
|
return new DownloadContent.Builder()
|
||||||
.setKind(DownloadContent.KIND_FONT)
|
.setKind(DownloadContent.KIND_FONT)
|
||||||
|
|
|
@ -206,4 +206,56 @@ public class TestDownloadContentCatalog {
|
||||||
Assert.assertTrue(catalog.getScheduledDownloads().contains(content4));
|
Assert.assertTrue(catalog.getScheduledDownloads().contains(content4));
|
||||||
Assert.assertTrue(catalog.getScheduledDownloads().contains(content5));
|
Assert.assertTrue(catalog.getScheduledDownloads().contains(content5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scenario: Calling rememberFailure() on a catalog with varying values
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testRememberingFailures() {
|
||||||
|
DownloadContentCatalog catalog = new DownloadContentCatalog(mock(AtomicFile.class));
|
||||||
|
Assert.assertFalse(catalog.hasCatalogChanged());
|
||||||
|
|
||||||
|
DownloadContent content = new DownloadContent.Builder().build();
|
||||||
|
Assert.assertEquals(0, content.getFailures());
|
||||||
|
|
||||||
|
catalog.rememberFailure(content, 42);
|
||||||
|
Assert.assertEquals(1, content.getFailures());
|
||||||
|
Assert.assertTrue(catalog.hasCatalogChanged());
|
||||||
|
|
||||||
|
catalog.rememberFailure(content, 42);
|
||||||
|
Assert.assertEquals(2, content.getFailures());
|
||||||
|
|
||||||
|
// Failure counter is reset if different failure has been reported
|
||||||
|
catalog.rememberFailure(content, 23);
|
||||||
|
Assert.assertEquals(1, content.getFailures());
|
||||||
|
|
||||||
|
// Failure counter is reset after successful download
|
||||||
|
catalog.markAsDownloaded(content);
|
||||||
|
Assert.assertEquals(0, content.getFailures());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scenario: Content has failed multiple times with the same failure type.
|
||||||
|
*
|
||||||
|
* Verify that:
|
||||||
|
* * Content is marked as permanently failed
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testContentWillBeMarkedAsPermanentlyFailedAfterMultipleFailures() {
|
||||||
|
DownloadContentCatalog catalog = new DownloadContentCatalog(mock(AtomicFile.class));
|
||||||
|
|
||||||
|
DownloadContent content = new DownloadContent.Builder().build();
|
||||||
|
Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
catalog.rememberFailure(content, 42);
|
||||||
|
|
||||||
|
Assert.assertEquals(i + 1, content.getFailures());
|
||||||
|
Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog.rememberFailure(content, 42);
|
||||||
|
Assert.assertEquals(10, content.getFailures());
|
||||||
|
Assert.assertEquals(DownloadContent.STATE_FAILED, content.getState());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче