🚧 Expose more information about not-loaded elements
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Fri, 24 Jul 2026 14:03:56 +0000 (16:03 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 4 Oct 2026 18:54:51 +0000 (20:54 +0200)
src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt
src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt

index 83178ae..7615776 100644 (file)
@@ -32,10 +32,12 @@ class DefaultElementLoader(private val freenetInterface: FreenetInterface, ticke
                override fun shouldCancel(uri: FreenetURI, mimeType: String, size: Long): Boolean {
                        if (size > 2097152) {
                                logger.fine { "Canceling download of $uri because it’s > 2 MiB." }
+                               rejectedElements += uri.toString() to RejectionReason.SizeTooLarge
                                return true
                        }
                        if (!mimeType.startsWith("image/") && !mimeType.startsWith("text/html")) {
                                logger.fine { "Canceling download of $uri because of its MIME type, $mimeType." }
+                               rejectedElements += uri.toString() to RejectionReason.InvalidMediaType
                                return true
                        }
                        return false
@@ -103,6 +105,25 @@ class DefaultElementLoader(private val freenetInterface: FreenetInterface, ticke
                return LinkedElement(link, loading = true)
        }
 
+       val loadingElements: Set<String>
+               get() = loadingLinks.asMap().keys
+
+       val loadedElements: Set<LinkedElement>
+               get() = elementCache.asMap().values.toSet()
+
+       val rejectedElements: Map<String, RejectionReason>
+               field = mutableMapOf()
+
+       val failedElements: Set<String>
+               get() = failureCache.asMap().keys
+
+       enum class RejectionReason {
+
+               InvalidMediaType,
+               SizeTooLarge,
+
+       }
+
 }
 
 private fun String.decode() = try { URLDecoder.decode(this, "UTF-8")!! } catch (e: RuntimeException) { freenet.support.Logger.error(DefaultElementLoader::class.java, "Could not decode %s!".format(this), e); throw e }
index f108371..eb9ee15 100644 (file)
@@ -2,28 +2,35 @@ package net.pterodactylus.sone.core
 
 import com.google.common.base.Ticker
 import freenet.keys.FreenetURI
+import java.io.ByteArrayOutputStream
+import java.util.concurrent.TimeUnit.MINUTES
+import java.util.concurrent.atomic.AtomicReference
+import java.util.logging.Handler
+import java.util.logging.Level.ALL
+import java.util.logging.LogRecord
+import java.util.logging.Logger
+import kotlin.math.min
+import net.pterodactylus.sone.core.DefaultElementLoader.RejectionReason.InvalidMediaType
+import net.pterodactylus.sone.core.DefaultElementLoader.RejectionReason.SizeTooLarge
 import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback
-import net.pterodactylus.sone.test.*
+import net.pterodactylus.sone.test.dummyHighLevelSimpleClientCreator
+import net.pterodactylus.sone.test.everything
+import net.pterodactylus.sone.test.handleMatcher
+import net.pterodactylus.sone.test.silencedLogging
 import org.hamcrest.Description
 import org.hamcrest.Matcher
 import org.hamcrest.MatcherAssert.assertThat
 import org.hamcrest.Matchers.allOf
 import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.containsInAnyOrder
 import org.hamcrest.Matchers.containsString
 import org.hamcrest.Matchers.equalTo
 import org.hamcrest.Matchers.hasEntry
+import org.hamcrest.Matchers.hasItem
 import org.hamcrest.Matchers.not
 import org.hamcrest.TypeSafeDiagnosingMatcher
 import org.junit.Rule
 import org.junit.Test
-import java.io.ByteArrayOutputStream
-import java.util.concurrent.TimeUnit.MINUTES
-import java.util.concurrent.atomic.AtomicReference
-import java.util.logging.Handler
-import java.util.logging.Level.ALL
-import java.util.logging.LogRecord
-import java.util.logging.Logger
-import kotlin.math.min
 
 /**
  * Unit test for [DefaultElementLoaderTest].
@@ -223,6 +230,59 @@ class DefaultElementLoaderTest {
                }
        }
 
+       @Test
+       fun `element loader exposes all currently loading elements`() {
+               runWithCallback("KSK@test") { elementLoader, _, _, _ ->
+                       elementLoader.loadElement("KSK@test")
+                       assertThat(elementLoader.loadingElements, containsInAnyOrder("KSK@test"))
+               }
+       }
+
+       @Test
+       fun `element loader exposes all currently loaded elements`() {
+               runWithCallback("KSK@test") { elementLoader, _, callback, _ ->
+                       callback.loaded(FreenetURI("KSK@test"), "image/png", read("/static/images/unknown-image-0.png"))
+                       assertThat(elementLoader.loadedElements, containsInAnyOrder(
+                               isLinkedElement(equalTo("KSK@test"), allOf(
+                                       hasEntry("type", "image"), hasEntry("size", 2451), hasEntry("sizeHuman", "2 KiB"),
+                               ))
+                       ))
+               }
+       }
+
+       @Test
+       fun `element loader no longer exposes an element as loading once it has been loaded`() {
+               runWithCallback("KSK@test") { elementLoader, _, callback, _ ->
+                       elementLoader.loadElement("KSK@test")
+                       callback.loaded(FreenetURI("KSK@test"), "image/png", read("/static/images/unknown-image-0.png"))
+                       assertThat(elementLoader.loadingElements, not(hasItem("KSK@test")))
+               }
+       }
+
+       @Test
+       fun `element loader exposes rejected element when media type does not match`() {
+               runWithCallback("KSK@test") { elementLoader, _, callback, _ ->
+                       callback.shouldCancel(FreenetURI("KSK@test"), "application/x-test-data", 1234)
+                       assertThat(elementLoader.rejectedElements, hasEntry("KSK@test", InvalidMediaType))
+               }
+       }
+
+       @Test
+       fun `element loader exposes rejected element when data is too large`() {
+               runWithCallback("KSK@test") { elementLoader, _, callback, _ ->
+                       callback.shouldCancel(FreenetURI("KSK@test"), "image/png", 12345678)
+                       assertThat(elementLoader.rejectedElements, hasEntry("KSK@test", SizeTooLarge))
+               }
+       }
+
+       @Test
+       fun `element loader exposes failed element when element cannot be loaded`() {
+               runWithCallback("KSK@test") { elementLoader, _, callback, _ ->
+                       callback.failed(FreenetURI("KSK@test"))
+                       assertThat(elementLoader.failedElements, hasItem("KSK@test"))
+               }
+       }
+
        private fun read(resource: String): ByteArray =
                javaClass.getResourceAsStream(resource)?.use { input ->
                        ByteArrayOutputStream().use {
@@ -253,7 +313,7 @@ class DefaultElementLoaderTest {
 
 }
 
-private fun runWithCallback(requestUri: String, ticker: Ticker = createTicker(), callbackAction: (elementLoader: ElementLoader, linkedElement: LinkedElement, callback: BackgroundFetchCallback, fetchedUris: List<FreenetURI>) -> Unit) {
+private fun runWithCallback(requestUri: String, ticker: Ticker = createTicker(), callbackAction: (elementLoader: DefaultElementLoader, linkedElement: LinkedElement, callback: BackgroundFetchCallback, fetchedUris: List<FreenetURI>) -> Unit) {
        val fetchedUris = mutableListOf<FreenetURI>()
        val callback = AtomicReference<BackgroundFetchCallback>()
        val freenetInterface = overrideStartFetch { uri, backgroundFetchCallback ->