package dev.fileworkbench.testing import dev.fileworkbench.core.PreviewException import dev.fileworkbench.core.browser.* import java.io.Closeable import java.io.IOException // Deliberately small doubles, not Android stubs for compiling the application. // The production listing block below is inserted verbatim by test-folder-listing.py. private object Document { const val COLUMN_DOCUMENT_ID = "document_id" } private object DocumentsContract { const val EXTRA_LOADING = "loading" const val EXTRA_INFO = "info" const val EXTRA_ERROR = "error" fun buildDocumentUriUsingTree(tree: String, id: String) = "$tree/document/$id" } private class Extras(private val values: Map = emptyMap()) { fun getBoolean(key: String, fallback: Boolean) = values[key] as? Boolean ?: fallback fun getString(key: String) = values[key] as? String } // Test-only representations for the diagnostic expression. They do not simulate // Android URI grants, provider availability or OS behaviour. private data class TestUri(val value: String, val authority: String? = "fixture.provider") private object Build { object VERSION { const val SDK_INT = 36 } } private object UriReadGrants { var retainedRead = false var failLookup = false @Suppress("UNUSED_PARAMETER") fun hasPersistentRead(resolver: TestResolver, tree: String): Boolean { if (failLookup) throw SecurityException("fixture grant lookup failure") return retainedRead } } private data class Row(val id: String?, val name: String = "sample.txt") private class TestCursor( val rows: List, val extras: Extras = Extras(), private val failOnMove: Boolean = false, ) : Closeable { var index = -1 var closeCount = 0 fun moveToNext(): Boolean { if (failOnMove) throw IOException("fixture read failure") return ++index < rows.size } fun string(column: String): String? { check(column == Document.COLUMN_DOCUMENT_ID) return rows[index].id } override fun close() { closeCount++ } } private class TestSignal(private val cancelled: Boolean = false) { fun throwIfCanceled() { if (cancelled) throw java.util.concurrent.CancellationException("fixture cancellation") } } private class TestResolver(private val cursor: TestCursor?) { var queries = 0 @Suppress("UNUSED_PARAMETER") fun query(uri: TestUri, projection: Array, selection: String?, arguments: Array?, order: String?, signal: TestSignal): TestCursor? { queries++ return cursor } } private fun entry(cursor: TestCursor, uri: String) = BrowserEntry(uri, cursor.rows[cursor.index].name, "text/plain") private fun readListing(resolver: TestResolver, signal: TestSignal = TestSignal()): BrowserListing = run { val tree = "fixture:root" val children = TestUri("fixture:root/children") val PROJECTION = arrayOf(Document.COLUMN_DOCUMENT_ID) /* PRODUCTION_LISTING_BLOCK */ } private inline fun expectThrows(block: () -> Unit): T { try { block() } catch (e: Throwable) { check(e is T) { "Expected ${T::class.java.simpleName}, got $e" } return e } error("Expected ${T::class.java.simpleName}") } private fun read(cursor: TestCursor): BrowserListing { val resolver = TestResolver(cursor) try { return readListing(resolver) } finally { check(cursor.closeCount == 1) { "Cursor must close exactly once" } check(resolver.queries == 1) { "Listing must issue exactly one query" } } } fun main() { var passed = 0 var failed = 0 fun test(name: String, block: () -> Unit) { try { block(); passed++; println("PASS $name") } catch (e: Throwable) { failed++; println("FAIL $name: ${e.javaClass.simpleName}: ${e.message}") } } test("normal rows without extras are not rejected") { val listing = read(TestCursor(listOf(Row("file")))) check(listing.entries.map { it.name } == listOf("sample.txt")) check(!listing.truncated && !listing.providerLoading && listing.information == null) } test("empty cursor without extras is a valid empty directory") { val listing = read(TestCursor(emptyList())) check(listing.entries.isEmpty() && !listing.truncated && !listing.providerLoading && listing.information == null) } test("information-only cursor keeps rows and message") { val listing = read(TestCursor(listOf(Row("file")), Extras(mapOf("info" to "Cached files")))) check(listing.entries.size == 1 && listing.information == "Cached files") } test("loading cursor without error keeps partial rows") { val listing = read(TestCursor(listOf(Row("file")), Extras(mapOf("loading" to true)))) check(listing.entries.size == 1 && listing.providerLoading && listing.information == null) } test("loading cursor may initially contain no rows") { val listing = read(TestCursor(emptyList(), Extras(mapOf("loading" to true)))) check(listing.entries.isEmpty() && listing.providerLoading && listing.information == null) } test("provider error takes precedence over info without discarding available rows") { val listing = read(TestCursor(listOf(Row("file")), Extras(mapOf("info" to "Cached files", "error" to "Some files are unavailable")))) check(listing.entries.size == 1 && listing.information == "Some files are unavailable") } test("actual null query result still raises the no-listing error") { val resolver = TestResolver(null) val error = expectThrows { readListing(resolver) } check(error.message?.startsWith("[SAF-D1/NULL_CURSOR]") == true) check(error.message?.contains("Provider: fixture.provider") == true) check(error.message?.contains("Android API: 36; retained read grant: false") == true) check(error.message?.contains("fixture:root/children") == false) check(resolver.queries == 1) } test("cursor closes when row iteration throws") { val cursor = TestCursor(listOf(Row("file")), failOnMove = true) expectThrows { read(cursor) } check(cursor.closeCount == 1) } test("cursor closes on cancellation during iteration") { val cursor = TestCursor(listOf(Row("file"))) expectThrows { readListing(TestResolver(cursor), TestSignal(cancelled = true)) } check(cursor.closeCount == 1) } test("entry limit still marks an oversized directory truncated") { val listing = read(TestCursor((0..BrowserLimits.MAX_ENTRIES).map { Row("file-$it") })) check(listing.entries.size == BrowserLimits.MAX_ENTRIES && listing.truncated) } test("exactly the entry limit is not marked truncated") { val listing = read(TestCursor((0 until BrowserLimits.MAX_ENTRIES).map { Row("file-$it") })) check(listing.entries.size == BrowserLimits.MAX_ENTRIES && !listing.truncated) } test("duplicate provider IDs remain deduplicated") { val listing = read(TestCursor(listOf(Row("file"), Row("file")))) check(listing.entries.size == 1) } test("missing and oversized document IDs are skipped") { val listing = read(TestCursor(listOf(Row(null), Row("x".repeat(8193)), Row("file")))) check(listing.entries.size == 1 && listing.entries.single().id.endsWith("/file")) } test("provider messages still use the shared label sanitizer") { val listing = read(TestCursor(emptyList(), Extras(mapOf("error" to "Warning\nunsafe\u202E")))) check(listing.information == "Warning_unsafe_") } test("diagnostic reflects a retained read grant without treating it as successful access") { UriReadGrants.retainedRead = true try { val error = expectThrows { readListing(TestResolver(null)) } check(error.message?.contains("retained read grant: true") == true) } finally { UriReadGrants.retainedRead = false } } test("unavailable permission diagnostic cannot replace the original null-cursor error") { UriReadGrants.failLookup = true try { val error = expectThrows { readListing(TestResolver(null)) } check(error.message?.startsWith("[SAF-D1/NULL_CURSOR]") == true) check(error.message?.contains("retained read grant: unknown") == true) } finally { UriReadGrants.failLookup = false } } println("Folder-listing host regression: $passed passed, $failed failed. NOT an Android/device test.") check(failed == 0) { "$failed regression checks failed" } }