Files
fileworkbench/scripts/regressions/FolderListingHarness.kt.template
T
zemion db873063f8
Verify source and build Android / android (push) Canceled after 0s
initial commit v 0.2.0
2026-09-24 22:49:27 +02:00

173 lines
8.5 KiB
Plaintext

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<String, Any?> = 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<Row>,
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<String>, selection: String?, arguments: Array<String>?, 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 <reified T : Throwable> 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<PreviewException> { 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<IOException> { read(cursor) }
check(cursor.closeCount == 1)
}
test("cursor closes on cancellation during iteration") {
val cursor = TestCursor(listOf(Row("file")))
expectThrows<java.util.concurrent.CancellationException> { 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<PreviewException> { 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<PreviewException> { 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" }
}