OutputPublisher's model of SAF is asserted only against fakes built to match it #226
Closed
opened 2026-09-06 02:53:14 +00:00 by JMR-dev
·
2 comments
No Branch/Tag Specified
main
fix/102-picker-back-press-overshoot
fix/268-saf-picker-determinism
feat/ogg-vorbis-libvorbis
feat/expedited-conversion-work
fix/gate-cache-in-worktrees
chore/gate-runs-shellcheck
test/publish-delete-arm-real-provider
docs/e8-instrumented-coverage
docs/api37-carrier-count-drift
docs/e7-second-constraint
test/publish-to-a-real-saf-destination
fix/api37-report-match-line
fix/api37-task-snapshot-crash
test/join-failure-message-on-device
docs/e2e-read-findings-e7
test/cancelling-a-running-export
test/reattach-to-a-running-job
test/content-uri-reaches-ffmpeg
fix/launcher-wiring-waits-for-the-pick
fix/cancel-tests-need-a-slower-encode
test/cancelling-a-running-join
test/cancelling-a-running-session
test/notification-cancel-action
test/ffmpeg-progress-is-observed
test/fallback-asserts-the-path
test/flac-and-opus-assert-their-format
docs/e2e-read-findings
docs/wave4-coverage-numbers
fix/injectable-startup-sweep-scope
test/session-outcome-seam
test/launcher-callback-identity
test/theme-follows-system-dark
test/audio-drop-arm
fix/rotation-waits-for-recreation
fix/convert-guards-on-ready
test/retry-save-mime
test/hardware-progress-reaches-workmanager
test/ffprobe-mapping-seam
test/device-codec-enumeration-seam
test/unknown-container-row
test/null-message-fallbacks
test/cancel-reaches-workmanager
docs/coverage-wave3-recovery
test/concat-engine-seam
docs/coverage-wave3
test/mediaprobe-merge-seam
test/adaptive-shell-wiring
test/aac-audio-args
test/notification-progress-text
test/media3-muxer-guard
test/hardware-fallback-and-cancellation
test/unprobeable-join-clip
test/one-branch-outcomes
test/foreground-type-regimes
fix/bound-wedge-diagnostics
docs/coverage-wave2
test/screen-wiring
test/viewmodel-setters
test/join-state-mapping
test/conversion-state-mapping
test/dedupe-user-messages
fix/restore-stack-merges
test/refused-jobs
test/concatworker-failure-arms
test/container-capabilities-audio
test/readspec-enum-fallbacks
test/outputpublisher-seams
test/mediaprobe-track-seam
test/outputpublisher-partial-branches
test/fake-provider-scaffolding
docs/coverage-read-findings
chore/gitignore-kotlin
test/bound-the-hangs
docs/coverage-remeasure
ci/baseline-counter-precision
fix/invalid-suggestion-chip
ci/wedged-leg-report
fix/reattachment-overwrites-pick
test/theme-live-branches
fix/failed-save-retry
fix/empty-composition-crash
ci/advisory-failure-report
docs/seven-run-counts
test/release-permission-guard
ci/build-workflow-permissions
docs/api37-point-release
docs/benchmark-populate-path
fix/dead-assertion-probe-test
ci/actionlint
test/device-codecs-encode-consequence
fix/sdkmanager-pipefail
docs/readme-restart-claim
fix/saf-picker-root-discovery
fix/probe-dispatcher-seam
test/media3engine-mime-tables
test/mediaprobe-pure-helpers
fix/codec-vocabulary-drift
docs/robolectric-rationale-correction
docs/api37-advisory-counts
test/r38-8-saf-e2e
test/r38-7-join-states
test/r38-6-conversion-states
test/r38-5-state-seam
fix/jacoco-robolectric-coverage
docs/instrumented-tests-correction
test/r38-2-filecard
test/r38-4-advanced-picker
test/r38-3-pickers
tools/file-issue-script
tools/api-37-emulator
fix/review-app-gaps
docs/review-corrections
No results found.
Labels
Clear labels
above-cut
accessibility
backlog
bug
confirmed
documentation
duplicate
enhancement
good first issue
help wanted
invalid
plausible
question
sev:high
sev:low
sev:medium
wontfix
Worked autonomously overnight: local, JVM-verifiable, no product decision
Barrier affecting people with disabilities
Held for manual review: product/UX call, CI/workflow, hardware, or unverifiable here
Something isn't working
Reviewer demonstrated the defect
Improvements or additions to documentation
This issue or pull request already exists
New feature or request
Good for newcomers
Extra attention is needed
This doesn't seem right
Reviewer could not fully demonstrate it; treat as unproven
Further information is requested
High severity
Low severity
Medium severity
This will not be worked on
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: JMR-dev/LibreMediaConverter#226
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Filed from the 2026-09-05 e2e read of the instrumented suite on
main@4b02294.OutputPublisher.publishis where the user's file is written. Its safety net turns on a claim about SAF that is asserted only by a fake built to match it.SafPickerRoundTripTest's KDoc says the position plainly:Two parts, with very different costs. They are one ticket because they share a fixture and (a) is most of the work for (b); split them if (b) is deferred.
(a)
publishhas never met a realDocumentsProvider— cheap, headless, CI-stableOutputPublisherPublishTestis Robolectric withFakeSafProvider+FakePlainProvider, registered throughregisterProvider(..., asDocumentsProvider = true). That flag is what makesDocumentsContract.isDocumentUrianswer true — so the branch guarding the delete is decided by a test-only registration, not by a provider.FixtureDocumentsProvideralready exists in the instrumented suite and is a realDocumentsProviderbehind a realContentResolver. It is read-only today: root flags areFLAG_LOCAL_ONLY, the root document's flags are0, and there is nocreateDocument.Extending it is small and self-contained:
Root.FLAG_SUPPORTS_CREATEon the rootDocument.FLAG_DIR_SUPPORTS_CREATEon the root documentcreateDocument(parentDocumentId, mimeType, displayName)creating a real zero-byte fileopenDocumenthonouring"w"deleteDocument+Document.FLAG_SUPPORTS_DELETEThen drive
publishat a URI from that provider and assert the three behaviours the fakes currently assert: a copy that fails partway deletes the document, a destination that already held bytes is not deleted, and a non-document URI is left alone.Mind the language constraint.
FixtureDocumentsProvideris the module's only Java file on purpose — it runs in a bareorg.libremediaconverter.testprocess with no Kotlin stdlib, and akotlin.*reference givesNoClassDefFoundError: kotlin/jvm/internal/Intrinsics. Its KDoc says so. Keep the additions Java andandroidx-free.Mutation: drop the
DocumentsContract.isDocumentUriguard indeletePartialOutput. Against a real provider the non-document case now deletes; the fake-based test would catch this too, so pair it with one only the real provider can bite — e.g. removeDocument.FLAG_SUPPORTS_DELETEand confirm the cleanup path reports rather than silently succeeding.(b) The premise itself, which nothing has ever observed
The test manufactures the precondition.
publish's KDoc rests on the same claim and calls it settled:destinationIsKnownEmptyreadsOpenableColumns.SIZEand authorises the delete only on a positive zero. Its own KDoc is careful that "I could not tell" must never authorise one — which is correct, and is exactly what makes the premise load-bearing: if stock DocumentsUI hands back a document that reports no size, or a non-zero one,destinationIsKnownEmptyreturns false,deletePartialOutputnever runs, and the fix for the truncated-file defect (D4) is inert in production while every test stays green.Nothing on any source set has watched real DocumentsUI do this.
This is O3-shaped: record the answer either way
The measurement is small — create a document through the real
ACTION_CREATE_DOCUMENTcontract against stock DocumentsUI (not the fixture provider), and before writing anything assert:DocumentsContract.isDocumentUri(context, uri)is true, andOpenableColumns.SIZEis present and0.A "no" is a defect, not a failed test. A written "stock DocumentsUI reports X, so the guard cannot fire, here is the measurement" is the successful outcome if it goes that way, and it changes
publishrather than the test.Cost, honestly: this half needs real DocumentsUI, so it pays #190's flake tax and lands next to the picker test that has already cost #80, #93 and #96. That is the whole reason it is separable from (a) — do (a) regardless; do (b) when someone is willing to own a second system-UI-driving test.
Related
docs/defect-audit.mdD4 — the truncated-file defect this guard fixedPart (a) cannot be done, and the reason changes this ticket. Measured on a local API 34 emulator, three approaches, all blocked by the same platform rule.
This ticket splits into a cheap headless half (a) and an expensive picker-driven half (b), on the premise that a real
DocumentsProvidercan be reached without DocumentsUI. It cannot. (a) is not cheaper than (b) — it is (b).What was tried
1. A second, unprotected
DocumentsProviderin the instrumentation APK.FixtureDocumentsProvideris behindMANAGE_DOCUMENTS; the idea was a sibling declaration without it, since nothing needs to pick from it. The platform refuses to install it at all:Any provider carrying the
DOCUMENTS_PROVIDERintent filter must hold that permission. And the filter is not optional:DocumentsContract.isDocumentUrireturns false without it, which is precisely the branch guardingdeletePartialOutput— so a provider without the filter tests nothing this ticket is about.2. Borrowing the test APK's identity. The instrumentation APK owns the provider, so
Instrumentation.getContext()looked like a way in. It is not — instrumentation runs in the target app's process, so test code carries the app's uid however theContextwas obtained:3.
uiAutomation.adoptShellPermissionIdentity("android.permission.MANAGE_DOCUMENTS"). Same denial, verbatim. The check is not "do you holdMANAGE_DOCUMENTS" but "do you hold a URI grant from the picker", and shell identity does not satisfy it.What that means
The message in 2 and 3 is the answer: a documents provider is reachable only through a grant issued by
ACTION_OPEN_DOCUMENT/ACTION_CREATE_DOCUMENT. So any test ofpublishagainst a realDocumentsProvidermust drive DocumentsUI, and pays #190's flake tax.The work is therefore one item, not two, and its cost is (b)'s. The natural home is
SafPickerRoundTripTest, which already drives DocumentsUI, already owns the retry and readable-screen machinery that took #80, #93 and #96 to get right, and currently stops atReady— the same class could carry aCREATE_DOCUMENTround trip.(b)'s question is unchanged and still worth answering, and it is now the whole ticket: does stock DocumentsUI hand back a pre-created, positively-zero-byte document?
OutputPublisherPublishTest.kt:94-96manufactures that precondition under a comment asserting it is how SAF behaves, anddestinationIsKnownEmpty→deletePartialOutput— D4's fix — is inert in production if it is false.Not done, deliberately
I wrote the provider extension (
createDocument,"w"mode,deleteDocument, a refusing document, recorded deletes) and reverted it. It is dead code until something can reach it, and this repo does not keep speculative test scaffolding. The shape is in this comment if the picker-driven version is picked up.Re-checked against
mainatb3d4318, with #245 merged. The finding stands; the cost argument that came with it does not.E7 is untouched
#245 changes zero lines in
app/src/androidTest/AndroidManifest.xml,FixtureDocumentsProvider.javaorFixtureContentProvider.java. E7 is a platform rule — any provider carrying theDOCUMENTS_PROVIDERfilter must holdMANAGE_DOCUMENTS, and instrumentation runs in the target app's process so nothing can borrow the test APK's identity. Nothing in #245 bears on it.So this is still one picker-driven item, not two. There is no cheap headless half.
What changed is the price, which is what I actually declined on
I deferred this on cost, and #245 removes most of it:
SafPickerRoundTripTestmethods now carry@FailsOnEmulatorApi37(:329,:350), so the picker class is entirely off the API 37 gating leg. My specific objection — that a third picker-driven test would make the gating aborts worse — is now false. A test added here carries the marker too and cannot touch gating.forceStopThePickermakes the whole-picker retry reachable, whereUiObject2.click()could not see a tap that reached no window. That is reliability on API 33–36, which is where a test here would actually gate.system_serverevery time", which makes marking a new DocumentsUI-driving test the documented expectation rather than a special case.And a correction to what I put on #108
I wrote there that the abort "lands on a different test each time" and that the two victims shared no cause beyond timing. Execution order says otherwise —
safruns beforework:NotificationCancelActionTestfires aPendingIntentthroughsystem_server, so my two sightings of it dying are most likely the picker test's abort surfacing on the next test that needssystem_server. One cause, not two — which is what #245's four-run logcat read already argued, and my comment overstated the variety.Remaining cost, stated plainly
One more marker (baseline 5 → 6), so CI never verifies this at API 37 — the same position the picker test is now in, and the Pixel check is what covers it. Against that: the open question is unchanged and still worth answering, because if stock DocumentsUI does not hand back a pre-created, positively-zero-byte document, then
destinationIsKnownEmptynever returns true,deletePartialOutputnever runs, and D4's fix is inert in production while every test stays green.Taking it now.