Move exception utils to a separate class and add tests for it

This commit is contained in:
Mauricio Colli 2020-03-28 20:08:42 -03:00
parent 65cd9751d8
commit a1b9892c77
No known key found for this signature in database
GPG key ID: F200BFD6F29DDD85
6 changed files with 158 additions and 88 deletions

View file

@ -27,7 +27,7 @@ import org.schabi.newpipe.report.AcraReportSenderFactory;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
@ -173,7 +173,7 @@ public class App extends Application {
private boolean isThrowableIgnored(@NonNull final Throwable throwable) { private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
// Don't crash the application over a simple network problem // Don't crash the application over a simple network problem
return ExtractorHelper.hasAssignableCauseThrowable(throwable, return ExceptionUtils.hasAssignableCause(throwable,
// network api cancellation // network api cancellation
IOException.class, SocketException.class, IOException.class, SocketException.class,
// blocking code disposed // blocking code disposed
@ -182,7 +182,7 @@ public class App extends Application {
private boolean isThrowableCritical(@NonNull final Throwable throwable) { private boolean isThrowableCritical(@NonNull final Throwable throwable) {
// Though these exceptions cannot be ignored // Though these exceptions cannot be ignored
return ExtractorHelper.hasAssignableCauseThrowable(throwable, return ExceptionUtils.hasAssignableCause(throwable,
NullPointerException.class, IllegalArgumentException.class, // bug in app NullPointerException.class, IllegalArgumentException.class, // bug in app
OnErrorNotImplementedException.class, MissingBackpressureException.class, OnErrorNotImplementedException.class, MissingBackpressureException.class,
IllegalStateException.class); // bug in operator IllegalStateException.class); // bug in operator

View file

@ -23,7 +23,7 @@ import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.InfoCache;
import java.io.IOException; import java.io.IOException;
@ -200,7 +200,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
return true; return true;
} }
if (ExtractorHelper.isInterruptedCaused(exception)) { if (ExceptionUtils.isInterruptedCaused(exception)) {
if (DEBUG) { if (DEBUG) {
Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]"); Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
} }

View file

@ -41,6 +41,7 @@ import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SearchExtractor; import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
@ -764,7 +765,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
handleSuggestions(listNotification.getValue()); handleSuggestions(listNotification.getValue());
} else if (listNotification.isOnError()) { } else if (listNotification.isOnError()) {
Throwable error = listNotification.getError(); Throwable error = listNotification.getError();
if (!ExtractorHelper.hasAssignableCauseThrowable(error, if (!ExceptionUtils.hasAssignableCause(error,
IOException.class, SocketException.class, IOException.class, SocketException.class,
InterruptedException.class, InterruptedIOException.class)) { InterruptedException.class, InterruptedIOException.class)) {
onSuggestionError(error); onSuggestionError(error);

View file

@ -0,0 +1,82 @@
package org.schabi.newpipe.util
import java.io.IOException
import java.io.InterruptedIOException
class ExceptionUtils {
companion object {
/**
* @return if throwable is related to Interrupted exceptions, or one of its causes is.
*/
@JvmStatic
fun isInterruptedCaused(throwable: Throwable): Boolean {
return hasExactCause(throwable,
InterruptedIOException::class.java,
InterruptedException::class.java)
}
/**
* @return if throwable is related to network issues, or one of its causes is.
*/
@JvmStatic
fun isNetworkRelated(throwable: Throwable): Boolean {
return hasAssignableCause(throwable,
IOException::class.java)
}
/**
* Calls [hasCause] with the `checkSubtypes` parameter set to false.
*/
@JvmStatic
fun hasExactCause(throwable: Throwable, vararg causesToCheck: Class<*>): Boolean {
return hasCause(throwable, false, *causesToCheck)
}
/**
* Calls [hasCause] with the `checkSubtypes` parameter set to true.
*/
@JvmStatic
fun hasAssignableCause(throwable: Throwable?, vararg causesToCheck: Class<*>): Boolean {
return hasCause(throwable, true, *causesToCheck)
}
/**
* Check if throwable has some cause from the causes to check, or is itself in it.
*
* If `checkIfAssignable` is true, not only the exact type will be considered equals, but also its subtypes.
*
* @param throwable throwable that will be checked.
* @param checkSubtypes if subtypes are also checked.
* @param causesToCheck an array of causes to check.
*
* @see Class.isAssignableFrom
*/
@JvmStatic
tailrec fun hasCause(throwable: Throwable?, checkSubtypes: Boolean, vararg causesToCheck: Class<*>): Boolean {
if (throwable == null) {
return false
}
// Check if throwable is a subtype of any of the causes to check
causesToCheck.forEach { causeClass ->
if (checkSubtypes) {
if (causeClass.isAssignableFrom(throwable.javaClass)) {
return true
}
} else {
if (causeClass == throwable.javaClass) {
return true
}
}
}
val currentCause: Throwable? = throwable.cause
// Check if cause is not pointing to the same instance, to avoid infinite loops.
if (throwable !== currentCause) {
return hasCause(currentCause, checkSubtypes, *causesToCheck)
}
return false
}
}
}

View file

@ -52,7 +52,6 @@ import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -306,85 +305,4 @@ public final class ExtractorHelper {
} }
}); });
} }
/**
* Check if throwable have the cause that can be assignable from the causes to check.
*
* @see Class#isAssignableFrom(Class)
* @param throwable the throwable to be checked
* @param causesToCheck the causes to check
* @return whether the exception is an instance of a subclass of one of the causes
* or is caused by an instance of a subclass of one of the causes
*/
public static boolean hasAssignableCauseThrowable(final Throwable throwable,
final Class<?>... causesToCheck) {
// Check if getCause is not the same as cause (the getCause is already the root),
// as it will cause a infinite loop if it is
Throwable cause;
Throwable getCause = throwable;
// Check if throwable is a subclass of any of the filtered classes
final Class throwableClass = throwable.getClass();
for (Class<?> causesEl : causesToCheck) {
if (causesEl.isAssignableFrom(throwableClass)) {
return true;
}
}
// Iteratively checks if the root cause of the throwable is a subclass of the filtered class
while ((cause = throwable.getCause()) != null && getCause != cause) {
getCause = cause;
final Class causeClass = cause.getClass();
for (Class<?> causesEl : causesToCheck) {
if (causesEl.isAssignableFrom(causeClass)) {
return true;
}
}
}
return false;
}
/**
* Check if throwable have the exact cause from one of the causes to check.
*
* @param throwable the throwable to be checked
* @param causesToCheck the causes to check
* @return whether the exception is an instance of one of the causes
* or is caused by an instance of one of the causes
*/
public static boolean hasExactCauseThrowable(final Throwable throwable,
final Class<?>... causesToCheck) {
// Check if getCause is not the same as cause (the getCause is already the root),
// as it will cause a infinite loop if it is
Throwable cause;
Throwable getCause = throwable;
for (Class<?> causesEl : causesToCheck) {
if (throwable.getClass().equals(causesEl)) {
return true;
}
}
while ((cause = throwable.getCause()) != null && getCause != cause) {
getCause = cause;
for (Class<?> causesEl : causesToCheck) {
if (cause.getClass().equals(causesEl)) {
return true;
}
}
}
return false;
}
/**
* Check if throwable have Interrupted* exception as one of its causes.
*
* @param throwable the throwable to be checkes
* @return whether the throwable is caused by an interruption
*/
public static boolean isInterruptedCaused(final Throwable throwable) {
return ExtractorHelper.hasExactCauseThrowable(throwable,
InterruptedIOException.class,
InterruptedException.class);
}
} }

View file

@ -0,0 +1,69 @@
package org.schabi.newpipe.util
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.schabi.newpipe.util.ExceptionUtils.Companion.hasAssignableCause
import org.schabi.newpipe.util.ExceptionUtils.Companion.hasExactCause
import java.io.IOException
import java.io.InterruptedIOException
import java.net.SocketException
import javax.net.ssl.SSLException
class ExceptionUtilsTest {
@Test fun `assignable causes`() {
assertTrue(hasAssignableCause(Throwable(), Throwable::class.java))
assertTrue(hasAssignableCause(Exception(), Exception::class.java))
assertTrue(hasAssignableCause(IOException(), Exception::class.java))
assertTrue(hasAssignableCause(IOException(), IOException::class.java))
assertTrue(hasAssignableCause(Exception(SocketException()), IOException::class.java))
assertTrue(hasAssignableCause(Exception(IllegalStateException()), RuntimeException::class.java))
assertTrue(hasAssignableCause(Exception(Exception(IOException())), IOException::class.java))
assertTrue(hasAssignableCause(Exception(IllegalStateException(Exception(IOException()))), IOException::class.java))
assertTrue(hasAssignableCause(Exception(IllegalStateException(Exception(SocketException()))), IOException::class.java))
assertTrue(hasAssignableCause(Exception(IllegalStateException(Exception(SSLException("IO")))), IOException::class.java))
assertTrue(hasAssignableCause(Exception(IllegalStateException(Exception(InterruptedIOException()))), IOException::class.java))
assertTrue(hasAssignableCause(Exception(IllegalStateException(Exception(InterruptedIOException()))), RuntimeException::class.java))
assertTrue(hasAssignableCause(IllegalStateException(), Throwable::class.java))
assertTrue(hasAssignableCause(IllegalStateException(), Exception::class.java))
assertTrue(hasAssignableCause(Exception(IllegalStateException(Exception(InterruptedIOException()))), InterruptedIOException::class.java))
}
@Test fun `no assignable causes`() {
assertFalse(hasAssignableCause(Throwable(), Exception::class.java))
assertFalse(hasAssignableCause(Exception(), IOException::class.java))
assertFalse(hasAssignableCause(Exception(IllegalStateException()), IOException::class.java))
assertFalse(hasAssignableCause(Exception(NullPointerException()), IOException::class.java))
assertFalse(hasAssignableCause(Exception(IllegalStateException(Exception(Exception()))), IOException::class.java))
assertFalse(hasAssignableCause(Exception(IllegalStateException(Exception(SocketException()))), InterruptedIOException::class.java))
assertFalse(hasAssignableCause(Exception(IllegalStateException(Exception(InterruptedIOException()))), InterruptedException::class.java))
}
@Test fun `exact causes`() {
assertTrue(hasExactCause(Throwable(), Throwable::class.java))
assertTrue(hasExactCause(Exception(), Exception::class.java))
assertTrue(hasExactCause(IOException(), IOException::class.java))
assertTrue(hasExactCause(Exception(SocketException()), SocketException::class.java))
assertTrue(hasExactCause(Exception(Exception(IOException())), IOException::class.java))
assertTrue(hasExactCause(Exception(IllegalStateException(Exception(IOException()))), IOException::class.java))
assertTrue(hasExactCause(Exception(IllegalStateException(Exception(SocketException()))), SocketException::class.java))
assertTrue(hasExactCause(Exception(IllegalStateException(Exception(SSLException("IO")))), SSLException::class.java))
assertTrue(hasExactCause(Exception(IllegalStateException(Exception(InterruptedIOException()))), InterruptedIOException::class.java))
assertTrue(hasExactCause(Exception(IllegalStateException(Exception(InterruptedIOException()))), IllegalStateException::class.java))
}
@Test fun `no exact causes`() {
assertFalse(hasExactCause(Throwable(), Exception::class.java))
assertFalse(hasExactCause(Exception(), Throwable::class.java))
assertFalse(hasExactCause(SocketException(), IOException::class.java))
assertFalse(hasExactCause(IllegalStateException(), RuntimeException::class.java))
assertFalse(hasExactCause(Exception(SocketException()), IOException::class.java))
assertFalse(hasExactCause(Exception(IllegalStateException(Exception(IOException()))), RuntimeException::class.java))
assertFalse(hasExactCause(Exception(IllegalStateException(Exception(SocketException()))), IOException::class.java))
assertFalse(hasExactCause(Exception(IllegalStateException(Exception(InterruptedIOException()))), IOException::class.java))
}
}