diff options
Diffstat (limited to 'app/src/androidTest/java/org/terst/feedreader/readability')
| -rw-r--r-- | app/src/androidTest/java/org/terst/feedreader/readability/ReaderExtractorRetryTest.kt | 127 |
1 files changed, 127 insertions, 0 deletions
diff --git a/app/src/androidTest/java/org/terst/feedreader/readability/ReaderExtractorRetryTest.kt b/app/src/androidTest/java/org/terst/feedreader/readability/ReaderExtractorRetryTest.kt new file mode 100644 index 0000000..ef2d578 --- /dev/null +++ b/app/src/androidTest/java/org/terst/feedreader/readability/ReaderExtractorRetryTest.kt @@ -0,0 +1,127 @@ +package org.terst.feedreader.readability + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.runBlocking +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import java.util.concurrent.TimeUnit +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Extraction used to fail intermittently on perfectly ordinary articles. + * + * onPageFinished fires before the page has finished loading — observed at + * progress=25 — and injecting the extraction script at that point is a race: + * sometimes the DOM is ready enough, and sometimes evaluateJavascript hands + * back a bare "null" because the document is still being replaced. Measured at + * 2 failures in 6 attempts against the same URL, which is what produced a bug + * report reading only "extraction failed" for one article while others worked. + * + * The fix retries an unusable result rather than treating it as final. + * + * HONEST LIMITATION: this test does not reproduce that race. A page served from + * a local MockWebServer finishes loading before onPageFinished fires + * (progress=100 every time), and adding deliberately slow subresources does not + * help — WebView simply waits for them. The race needs a real remote page. + * Verified manually instead: against the reported URL the extractor scored 4/6 + * before the fix and 10/10 after. + * + * What this test IS worth keeping for: it asserts that repeated back-to-back + * extractions in one process all succeed, which guards the WebView + * create/destroy churn the retry loop now leans on, and would catch a + * regression that broke extraction outright. + */ +@RunWith(AndroidJUnit4::class) +class ReaderExtractorRetryTest { + + private lateinit var server: MockWebServer + + @Before + fun startServer() { + server = MockWebServer() + // A page served instantly from localhost finishes loading before + // onPageFinished fires (observed progress=100), which does NOT + // reproduce the bug. A real article is still pulling subresources at + // that point — the observed failure had progress=25. So the fixture + // references subresources the server deliberately dawdles over, which + // is what puts the document in the half-loaded state the extraction + // script used to be injected into. + server.dispatcher = object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + val path = request.path.orEmpty() + return when { + path.startsWith("/slow") -> + MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/javascript") + .setBody("/* slow */") + .setBodyDelay(3, TimeUnit.SECONDS) + else -> + MockResponse().setResponseCode(200) + .setHeader("Content-Type", "text/html") + .setBody(article) + } + } + } + server.start() + } + + @After + fun stopServer() { + server.shutdown() + } + + // Enough real prose that Readability is in no doubt there is an article + // here; the failures under test are about timing, not content. + private val article = buildString { + append("<html><head><title>A perfectly ordinary article</title>") + // Subresources the server is slow to answer, so the page is still + // loading when onPageFinished fires. + append("<script src=\"/slow-a.js\"></script>") + append("<script src=\"/slow-b.js\"></script>") + append("</head><body><article>") + append("<h1>A perfectly ordinary article</h1>") + repeat(12) { + append( + "<p>This paragraph exists so that Readability has a comfortably large " + + "body of text to score, well above any minimum content threshold, so " + + "that a failure to extract can only be a timing problem rather than a " + + "judgement about whether this page contains an article at all.</p>" + ) + } + append("</article></body></html>") + } + + @Test + fun repeatedExtractionOfTheSamePageAlwaysSucceeds() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val attempts = 8 + val failures = mutableListOf<String>() + + repeat(attempts) { i -> + val url = server.url("/article-$i").toString() + when (val r = runBlocking { ReaderExtractor.extractResult(context, url) }) { + is ExtractionResult.Success -> + assertTrue( + "attempt ${i + 1} returned empty content", + !r.article.content.isNullOrBlank() + ) + is ExtractionResult.Failure -> + failures += "attempt ${i + 1}: ${r.reason.diagnostic()}" + } + } + + assertTrue( + "extraction is not reliable — ${failures.size}/$attempts failed:\n" + + failures.joinToString("\n"), + failures.isEmpty() + ) + } +} |
