initial commit v 0.2.0
Verify source and build Android / android (push) Canceled after 0s

This commit is contained in:
2026-09-24 22:49:27 +02:00
commit db873063f8
129 changed files with 7512 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
package dev.fileworkbench.testing
import com.intellij.openapi.util.Disposer
import com.intellij.psi.PsiErrorElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.psi.KtPsiFactory
import java.io.File
/** Syntax only. This deliberately does not claim Android/Compose symbol or Gradle verification. */
fun main(args: Array<String>) {
val root = File(args.firstOrNull() ?: ".").canonicalFile
val disposable = Disposer.newDisposable()
try {
val environment = KotlinCoreEnvironment.createForProduction(disposable, CompilerConfiguration(), EnvironmentConfigFiles.JVM_CONFIG_FILES)
val factory = KtPsiFactory(environment.project, false)
var files = 0
var errors = 0
root.walkTopDown().onEnter { it.name !in setOf("build", "out", ".gradle", ".git") }
.filter { it.isFile && (it.extension == "kt" || it.extension == "kts") }.forEach { source ->
files++
val text = source.readText()
val parsed = factory.createFile(source.name, text)
PsiTreeUtil.findChildrenOfType(parsed, PsiErrorElement::class.java).forEach {
errors++
val line = text.take(it.textOffset).count { c -> c == '\n' } + 1
println("FAIL ${source.relativeTo(root)}:$line ${it.errorDescription}")
}
}
println("Kotlin PSI syntax: $files files, $errors parse errors. Android/Compose type resolution is NOT tested.")
check(errors == 0)
} finally { Disposer.dispose(disposable) }
}
+40
View File
@@ -0,0 +1,40 @@
$ErrorActionPreference = 'Stop'
$Root = Split-Path $PSScriptRoot -Parent
$Jar = Join-Path $Root 'gradle/wrapper/gradle-wrapper.jar'
$Work = Join-Path (Split-Path $Jar) "bootstrap.$([guid]::NewGuid().ToString('N'))"
$Temp = Join-Path $Work 'wrapper.jar'
$Expected = '7d3a4ac4de1c32b59bc6a4eb8ecb8e612ccd0cf1ae1e99f66902da64df296172'
$DistExpected = '6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854'
New-Item -ItemType Directory -Force -Path $Work | Out-Null
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -UseBasicParsing -Uri 'https://raw.githubusercontent.com/gradle/gradle/v8.14.5/gradle/wrapper/gradle-wrapper.jar' -OutFile $Temp
$Actual = (Get-FileHash -Algorithm SHA256 $Temp).Hash.ToLowerInvariant()
if ($Actual -ne $Expected) {
Write-Host 'Tag wrapper differs from release; retrieving the verified release distribution.'
$Dist = Join-Path $Work 'gradle.zip'
Invoke-WebRequest -UseBasicParsing -Uri 'https://services.gradle.org/distributions/gradle-8.14.5-bin.zip' -OutFile $Dist
if ((Get-FileHash -Algorithm SHA256 $Dist).Hash.ToLowerInvariant() -ne $DistExpected) {
throw 'Distribution checksum mismatch; refusing to execute.'
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
$Outer = [IO.Compression.ZipFile]::OpenRead($Dist)
try {
$Entries = @($Outer.Entries | Where-Object { $_.FullName -match '/gradle-wrapper-main-8\.14\.5\.jar$' })
if ($Entries.Count -ne 1) { throw 'Expected exactly one wrapper-main JAR.' }
$Main = Join-Path $Work 'wrapper-main.jar'
[IO.Compression.ZipFileExtensions]::ExtractToFile($Entries[0], $Main, $true)
} finally { $Outer.Dispose() }
$Inner = [IO.Compression.ZipFile]::OpenRead($Main)
try {
$Entry = $Inner.GetEntry('gradle-wrapper.jar')
if ($null -eq $Entry) { throw 'Embedded wrapper missing.' }
[IO.Compression.ZipFileExtensions]::ExtractToFile($Entry, $Temp, $true)
} finally { $Inner.Dispose() }
if ((Get-FileHash -Algorithm SHA256 $Temp).Hash.ToLowerInvariant() -ne $Expected) {
throw 'Embedded wrapper checksum mismatch; refusing to execute.'
}
}
Move-Item -Force $Temp $Jar
Write-Host 'Official Gradle 8.14.5 wrapper verified.'
} finally { if (Test-Path $Work) { Remove-Item -Recurse -Force $Work } }
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env sh
# Build tooling only. All downloaded executable bytes must match official release hashes.
set -eu
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
JAR="$ROOT/gradle/wrapper/gradle-wrapper.jar"
EXPECTED=7d3a4ac4de1c32b59bc6a4eb8ecb8e612ccd0cf1ae1e99f66902da64df296172
DIST_EXPECTED=6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854
mkdir -p "$(dirname "$JAR")"
WORK=$(mktemp -d "$ROOT/gradle/wrapper/bootstrap.XXXXXX")
trap 'rm -rf "$WORK"' EXIT HUP INT TERM
fetch() {
if command -v curl >/dev/null 2>&1; then
curl --fail --location --proto '=https' --tlsv1.2 --connect-timeout 15 --max-time 300 "$1" -o "$2"
elif command -v wget >/dev/null 2>&1; then
wget --https-only --timeout=30 "$1" -O "$2"
else
echo 'Install curl/wget, or generate the wrapper with an installed Gradle 8.14.5.' >&2
return 1
fi
}
checksum() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d ' ' -f 1
elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d ' ' -f 1
else echo 'No SHA-256 utility found; refusing unverified tooling.' >&2; return 1; fi
}
printf '%s\n' 'Fetching the official Gradle 8.14.5 wrapper; a pinned SHA-256 is required.'
fetch https://raw.githubusercontent.com/gradle/gradle/v8.14.5/gradle/wrapper/gradle-wrapper.jar "$WORK/wrapper.jar"
ACTUAL=$(checksum "$WORK/wrapper.jar")
if [ "$ACTUAL" != "$EXPECTED" ]; then
# A source tag can contain an older build's wrapper. Never execute that mismatch.
# The released distribution contains the exact wrapper as an embedded resource.
printf '%s\n' 'Tag wrapper does not match the release checksum; retrieving the verified release distribution.'
command -v unzip >/dev/null 2>&1 || {
echo 'Install unzip for this fallback, or generate the wrapper with installed Gradle 8.14.5.' >&2
exit 1
}
fetch https://services.gradle.org/distributions/gradle-8.14.5-bin.zip "$WORK/gradle.zip"
[ "$(checksum "$WORK/gradle.zip")" = "$DIST_EXPECTED" ] || {
echo 'Gradle distribution checksum mismatch; refusing to execute.' >&2; exit 1
}
ENTRY=$(unzip -Z1 "$WORK/gradle.zip" | grep '/gradle-wrapper-main-8\.14\.5\.jar$')
[ "$(printf '%s\n' "$ENTRY" | wc -l | tr -d ' ')" = 1 ] || {
echo 'Expected exactly one wrapper-main JAR in the verified release.' >&2; exit 1
}
unzip -p "$WORK/gradle.zip" "$ENTRY" > "$WORK/wrapper-main.jar"
unzip -p "$WORK/wrapper-main.jar" gradle-wrapper.jar > "$WORK/wrapper.jar"
[ "$(checksum "$WORK/wrapper.jar")" = "$EXPECTED" ] || {
echo 'Embedded wrapper checksum mismatch; refusing to execute.' >&2; exit 1
}
fi
mv "$WORK/wrapper.jar" "$JAR"
printf '%s\n' 'Official wrapper verified. The Gradle distribution is separately checksum-pinned.'
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Check this release's build pins and plugin IDs; NOT Gradle/Kotlin type checking."""
from pathlib import Path
import hashlib
import re
ROOT = Path(__file__).resolve().parent.parent
checks = 0
def verify(condition, message):
global checks
if not condition:
raise SystemExit("FAIL build configuration: " + message)
checks += 1
print("PASS " + message)
def text(name):
return (ROOT / name).read_text(encoding="utf-8")
def properties(name):
return dict(line.split("=", 1) for line in text(name).splitlines()
if "=" in line and not line.lstrip().startswith("#"))
DIST = "6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854"
JAR = "7d3a4ac4de1c32b59bc6a4eb8ecb8e612ccd0cf1ae1e99f66902da64df296172"
wrapper = properties("gradle/wrapper/gradle-wrapper.properties")
verify(wrapper["distributionUrl"].replace(chr(92), "") ==
"https://services.gradle.org/distributions/gradle-8.14.5-bin.zip", "Gradle distribution is exactly 8.14.5")
verify(wrapper["distributionSha256Sum"] == DIST, "official 8.14.5 distribution SHA-256 is pinned")
for script in ["scripts/bootstrap-wrapper.sh", "scripts/bootstrap-wrapper.ps1"]:
content = text(script)
verify(JAR in content and DIST in content and "v8.14.5/" in content and
"gradle-8.14.5-bin.zip" in content and "8.13" not in content,
script + " uses matching 8.14.5 downloads and hashes")
verify(properties("gradle/gradle-daemon-jvm.properties").get("toolchainVersion") == "17", "Gradle daemon requires JDK 17")
verify(text(".java-version").strip() == "17", "Java-version hint is 17")
verify("JavaVersion.VERSION_17" in text("settings.gradle.kts") and
'gradle.gradleVersion == "8.14.5"' in text("settings.gradle.kts"), "settings reject conflicting Gradle/JVM overrides")
for module in ["androidApp", "core", "platform-jvm"]:
verify("jvmToolchain(17)" in text(module + "/build.gradle.kts"), module + " uses toolchain 17")
android = text("androidApp/build.gradle.kts")
verify("sourceCompatibility = JavaVersion.VERSION_17" in android and
"targetCompatibility = JavaVersion.VERSION_17" in android, "Android Java bytecode targets 17")
verify('versionName = "0.2.3"' in android and "versionCode = 5" in android, "application version is 0.2.3/code 5")
# Deliberately limited to the project's simple version-catalog/alias structure.
root_build = text("build.gradle.kts")
catalog = text("gradle/libs.versions.toml")
plugin_section = catalog.split("[plugins]", 1)[1]
entries = re.findall(r'^([\w-]+)\s*=\s*\{\s*id\s*=\s*"([^"]+)"', plugin_section, re.M)
ids = dict(entries)
aliases = [a.replace(".", "-") for a in re.findall(r'alias\(libs\.plugins\.([\w.]+)\)', root_build)]
expected = {"com.android.application", "org.jetbrains.kotlin.android", "org.jetbrains.kotlin.multiplatform",
"org.jetbrains.kotlin.jvm", "org.jetbrains.kotlin.plugin.compose"}
resolved = [ids.get(a) for a in aliases]
verify(len(entries) == len(ids) == 5 and len(resolved) == 5 and set(resolved) == expected,
"five root plugin aliases resolve to five distinct expected IDs")
verify(not re.search(r'\b(?:id|kotlin)\s*\(', root_build), "root has no extra direct plugin requests")
verify('kotlin = "2.2.21"' in catalog and 'agp = "8.11.1"' in catalog, "Kotlin/AGP versions are unchanged")
verify("java-version: '17'" in text(".github/workflows/verify.yml"), "existing CI uses Java 17")
verify("[SAF-D1/NULL_CURSOR]" in text("androidApp/src/main/kotlin/dev/fileworkbench/android/browser/SafBrowserStore.kt") and
"Fileworkbench 0.2.3 · Browser diagnostics SAF-D1" in text("androidApp/src/main/kotlin/dev/fileworkbench/android/ui/BrowserScreen.kt"),
"source contains the diagnostic error and visible release marker")
verify("org.gradle.java.home" not in properties("gradle.properties"), "tracked Gradle properties contain no machine-specific JDK home")
jar = ROOT / "gradle/wrapper/gradle-wrapper.jar"
if jar.exists():
verify(hashlib.sha256(jar.read_bytes()).hexdigest() == JAR, "bundled official wrapper JAR matches its checksum")
else:
print("INFO wrapper JAR is not bundled; the first wrapper launch bootstraps the verified binary.")
print(f"Build-configuration checks: {checks} passed. Source checks only, not an Android build.")
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
KOTLIN_DIR="$(cd "$(dirname "$(command -v kotlinc)")/.." && pwd)"
mkdir -p out/syntax
kotlinc scripts/KotlinSyntaxCheck.kt -jvm-target 17 -cp "$KOTLIN_DIR/lib/kotlin-compiler.jar" -d out/syntax/check.jar
java -cp "out/syntax/check.jar:$KOTLIN_DIR/lib/*" dev.fileworkbench.testing.KotlinSyntaxCheckKt .
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Source-level invariants, not a substitute for Android lint, Gradle or device testing."""
from pathlib import Path
import xml.etree.ElementTree as ET
root = Path(__file__).resolve().parent.parent
android = '{http://schemas.android.com/apk/res/android}'
for xml in (root / 'androidApp/src').rglob('*.xml'):
ET.parse(xml)
manifest = ET.parse(root / 'androidApp/src/main/AndroidManifest.xml').getroot()
assert not manifest.findall('uses-permission'), 'Unexpected permission in app manifest'
application = manifest.find('application')
assert application.get(android+'allowBackup') == 'false'
service = application.find('service')
assert service.get(android+'isolatedProcess') == 'true'
assert service.get(android+'exported') == 'false'
provider = application.find('provider')
assert provider.get(android+'exported') == 'false'
for source in (root / 'core/src/commonMain').rglob('*.kt'):
content = source.read_text()
assert 'import android.' not in content and 'import java.' not in content, source
all_android = '\n'.join(p.read_text() for p in (root / 'androidApp/src/main/kotlin').rglob('*.kt'))
assert 'WebView(' not in all_android
assert 'Log.' not in all_android
print('PASS project invariants: XML, permission-free manifest, isolated PDF service, narrow FileProvider, portable core, no WebView/log calls.')
# Browser/picker entry points must not impersonate the system document chooser.
activities = {item.get(android+'name'): item for item in application.findall('activity')}
main = activities['.MainActivity']
picker = activities['.FilePickerActivity']
assert picker.get(android+'exported') == 'true'
assert picker.get(android+'launchMode', 'standard') == 'standard'
all_actions = {item.get(android+'name') for item in application.findall('.//action')}
assert 'android.intent.action.GET_CONTENT' in all_actions
assert 'android.intent.action.OPEN_DOCUMENT' not in all_actions
assert 'android.intent.action.OPEN_DOCUMENT_TREE' not in all_actions
assert any(item.get(android+'name') == 'android.intent.category.APP_FILES' for item in main.findall('.//category'))
assert any(item.get(android+'name') == 'android.intent.category.OPENABLE' for item in picker.findall('.//category'))
paths = ET.parse(root / 'androidApp/src/main/res/xml/file_paths.xml').getroot()
assert {(item.tag, item.get('path')) for item in paths} == {('cache-path', 'shared/'), ('cache-path', 'picked/')}
assert all('FixtureDocumentsProvider' not in item.get(android+'name', '') for item in application.findall('provider'))
for file in ['MainActivity.kt', 'FilePickerActivity.kt']:
content = (root / 'androidApp/src/main/kotlin/dev/fileworkbench/android' / file).read_text()
assert 'sanitized' in content
assert content.index('sanitized') < content.index('super.onCreate(savedInstanceState)')
copy_code = (root / 'androidApp/src/main/kotlin/dev/fileworkbench/android/browser/PickedFileStore.kt').read_text()
assert 'SelectionCopyBudget()' in copy_code
assert 'flags = Intent.FLAG_GRANT_READ_URI_PERMISSION' in copy_code
assert 'FLAG_GRANT_PREFIX_URI_PERMISSION' not in copy_code
assert 'FLAG_GRANT_WRITE_URI_PERMISSION' not in copy_code
assert 'FLAG_GRANT_PERSISTABLE_URI_PERMISSION' not in copy_code
print('PASS browser source invariants: APP_FILES/GET_CONTENT, standard picker activity, no system-picker impersonation, narrow copied paths, read-only results, sanitized activity defaults, debug fixture excluded from main manifest.')
# Regression guard for the reported instrumentation compile failure. This deliberately
# checks only the known constructor/use pattern; it is NOT Kotlin type checking.
import re
for source in (root / 'androidApp/src').rglob('*.kt'):
assert not re.search(r"\bPdfDocument\s*\(\s*\)\s*\.\s*use\b", source.read_text()), (
f'{source}: PdfDocument does not implement Closeable/AutoCloseable; use try/finally'
)
print('PASS PDF source regression guard: no PdfDocument().use call (pattern check only).')
# The release is intentionally pinned; prevent mixed old/new wrapper or plugin files.
import subprocess
import sys
subprocess.run([sys.executable, str(root / 'scripts/check-build-config.py')], check=True)
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Run the same @Test method bodies offline, omitting only JUnit/Kotlin runner annotations."""
from pathlib import Path
import re
import shutil
root = Path(__file__).resolve().parent.parent
out = root / "out/offline-tests/generated"
shutil.rmtree(out, ignore_errors=True)
out.mkdir(parents=True)
cases = []
for source in sorted(list((root / "core/src/commonTest").rglob("*Tests.kt")) + list((root / "platform-jvm/src/test").rglob("*Tests.kt"))):
text = source.read_text()
package = re.search(r"^package ([\w.]+)", text, re.M).group(1)
cls = re.search(r"^class (\w+Tests)", text, re.M).group(1)
for method in re.findall(r"@Test\s+fun (\w+)\(", text):
cases.append((f"{cls}.{method}", f"{package}.{cls}().{method}()"))
text = re.sub(r"^import (?:kotlin\.test|org\.junit)\.Test\s*$", "", text, flags=re.M).replace("@Test ", "")
(out / source.name).write_text(text)
runner = """package dev.fileworkbench.testing
fun main() {
val cases: List<Pair<String, () -> Unit>> = listOf(
"""
runner += ",\n".join(f' "{name}" to {{ {call} }}' for name, call in cases)
runner += """
)
var failed = 0
for ((name, run) in cases) {
try { run(); println("PASS $name") }
catch (e: Throwable) { failed++; println("FAIL $name: ${e.message}"); e.printStackTrace() }
}
println("RESULT: ${cases.size - failed}/${cases.size} checks passed")
check(failed == 0) { "$failed checks failed" }
}
"""
(out / "OfflineRunner.kt").write_text(runner)
print(f"Prepared {len(cases)} production-core/JVM checks.")
@@ -0,0 +1,172 @@
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" }
}
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Regression checks for the release-pin guard; no Gradle execution or downloads."""
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parent.parent
passed = 0
cases = [
("baseline", None, None, None, True),
("stale Gradle distribution", "gradle/wrapper/gradle-wrapper.properties", "gradle-8.14.5-bin", "gradle-9.7.1-bin", False),
("wrong daemon JVM", "gradle/gradle-daemon-jvm.properties", "toolchainVersion=17", "toolchainVersion=25", False),
("duplicate JVM alias", "build.gradle.kts", "alias(libs.plugins.kotlin.jvm) apply false",
"alias(libs.plugins.kotlin.jvm) apply false\n alias(libs.plugins.kotlin.jvm) apply false", False),
("catalog maps two aliases to JVM", "gradle/libs.versions.toml", 'id = "org.jetbrains.kotlin.android"',
'id = "org.jetbrains.kotlin.jvm"', False),
("extra direct plugin request", "build.gradle.kts", "plugins {",
'plugins {\n id("org.jetbrains.kotlin.jvm") apply false', False),
("stale shell bootstrap", "scripts/bootstrap-wrapper.sh", "v8.14.5/", "v8.13.0/", False),
("stale Windows bootstrap", "scripts/bootstrap-wrapper.ps1", "v8.14.5/", "v8.13.0/", False),
]
for name, relative, old, new, expect_success in cases:
with tempfile.TemporaryDirectory(prefix="fileworkbench-build-config-") as work:
target = Path(work) / "project"
shutil.copytree(ROOT, target, ignore=shutil.ignore_patterns("out", "build", ".gradle", ".git", ".idea", "local.properties", "__pycache__"))
if relative:
file = target / relative
source = file.read_text(encoding="utf-8")
assert old in source, name + ": expected fixture content missing"
file.write_text(source.replace(old, new, 1), encoding="utf-8")
else:
# Local IDE/SDK state is legitimate on the developer's machine.
# The source-package check must not accidentally prohibit ordinary builds.
(target / "local.properties").write_text("sdk.dir=/example/local/sdk\n", encoding="utf-8")
(target / ".idea").mkdir()
result = subprocess.run([sys.executable, str(target / "scripts/check-build-config.py")],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if (result.returncode == 0) != expect_success:
print(result.stdout)
raise SystemExit("FAIL " + name)
passed += 1
print("PASS " + name + (" accepted" if expect_success else " rejected"))
print(f"Build-guard regression: {passed}/{len(cases)} cases passed. Not a Gradle build.")
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Offline verification of the production common core + JVM ZIP adapter.
# Uses an installed Kotlin/JVM compiler and kotlin-test.jar; does not claim an Android Gradle build.
set -euo pipefail
cd "$(dirname "$0")/.."
command -v kotlinc >/dev/null || { echo 'Install Kotlin (kotlinc) and a JDK first.' >&2; exit 1; }
KOTLIN_BIN="$(command -v kotlinc)"
KOTLIN_DIR="$(cd "$(dirname "$KOTLIN_BIN")/.." && pwd)"
TEST_JAR="$KOTLIN_DIR/lib/kotlin-test.jar"
[ -f "$TEST_JAR" ] || { echo "kotlin-test.jar not found: $TEST_JAR" >&2; exit 1; }
mkdir -p out/offline-tests
python3 scripts/prepare-offline-tests.py
SOURCES=()
while IFS= read -r source; do SOURCES+=("$source"); done < <(find core/src/commonMain platform-jvm/src/main out/offline-tests/generated -name '*.kt' | sort)
kotlinc "${SOURCES[@]}" -jvm-target 17 -cp "$TEST_JAR" -include-runtime -d out/offline-tests/tests.jar
java -cp "out/offline-tests/tests.jar:$TEST_JAR" dev.fileworkbench.testing.OfflineRunnerKt | tee out/offline-tests/results.txt
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Run the actual SafBrowserStore cursor-reading block on JVM test doubles.
Requires a local Kotlin compiler and JDK; no SDK, Gradle, or downloads. This checks
Kotlin control flow and resource cleanup, NOT Android API compatibility, Binder,
URI grants, async IO, provider implementation, or device/UI behaviour.
"""
import argparse
from pathlib import Path
import shutil
import subprocess
ROOT = Path(__file__).resolve().parent.parent
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source', type=Path, default=ROOT / 'androidApp/src/main/kotlin/dev/fileworkbench/android/browser/SafBrowserStore.kt',
help='Optional historical source for reproducing the regression')
args = parser.parse_args()
compiler = shutil.which('kotlinc')
java = shutil.which('java')
if compiler is None or java is None:
parser.error('Install kotlinc and a JDK, then rerun this offline check.')
source = args.source.read_text(encoding='utf-8')
# Copy verbatim, rather than maintaining a second corrected implementation.
start_marker = ' val rows = ArrayList<BrowserEntry>()'
end_marker = ' BrowserListing(rows.distinctBy { it.id }, truncated, loading, information)'
if source.count(start_marker) != 1 or source.count(end_marker) != 1:
parser.error('Listing method changed shape; review and update the harness extraction boundaries.')
start = source.index(start_marker)
end = source.index(end_marker, start) + len(end_marker)
block = source[start:end]
template = (ROOT / 'scripts/regressions/FolderListingHarness.kt.template').read_text(encoding='utf-8')
assert template.count('/* PRODUCTION_LISTING_BLOCK */') == 1
out = ROOT / 'out/folder-listing'
out.mkdir(parents=True, exist_ok=True)
generated = out / 'FolderListingHarness.kt'
generated.write_text(template.replace('/* PRODUCTION_LISTING_BLOCK */', block), encoding='utf-8')
core = sorted(str(p) for p in (ROOT / 'core/src/commonMain').rglob('*.kt'))
jar = out / 'regression.jar'
print(f'Source under test: {args.source}', flush=True)
print('Extracted production cursor-reading block + real common models; Android APIs are test doubles.', flush=True)
compiled = subprocess.run([compiler, *core, str(generated), '-jvm-target', '17', '-include-runtime', '-d', str(jar)], cwd=ROOT)
if compiled.returncode:
raise SystemExit(compiled.returncode)
raise SystemExit(subprocess.run([java, '-cp', str(jar), 'dev.fileworkbench.testing.FolderListingHarnessKt'], cwd=ROOT).returncode)
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
python3 scripts/check-project.py
./gradlew --no-daemon :core:jvmTest :platform-jvm:test :androidApp:assembleDebug :androidApp:assembleDebugAndroidTest :androidApp:lintDebug
printf '%s\n' 'APK: androidApp/build/outputs/apk/debug/androidApp-debug.apk'