Skip to content

ADFA-5037: Stop install() from swallowing doInstall() failures - #1632

Merged
davidschachterADFA merged 5 commits into
stagefrom
bugfix/ADFA-5037-install-swallows-failure
Aug 6, 2026
Merged

ADFA-5037: Stop install() from swallowing doInstall() failures#1632
davidschachterADFA merged 5 commits into
stagefrom
bugfix/ADFA-5037-install-swallows-failure

Conversation

@davidschachterADFA

@davidschachterADFA davidschachterADFA commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • AssetsInstallationHelper.install() wraps doInstall() in runCatching, but doInstall()'s preInstall failure handling caught FileNotFoundException/ZipException/IOException and returned a Result.Failure value instead of throwing. runCatching only detects thrown exceptions, so that returned Failure was silently discarded and install() fell through to Result.Success regardless.
  • Each catch block now rethrows after its existing logging/flash/progress side-effect, so the failure actually reaches install()'s exception handling (including the existing MissingAssetsEntryException wrapping for the "missing/corrupt assets" case).
  • Found while independently re-verifying the ADFA-5005 fix on a debug build: SplitAssetsInstaller.preInstall() threw FileNotFoundException (external QA-testing zip not present), yet InstallationViewModel logged "Assets installation result: Success" and the app proceeded past onboarding with a half-installed toolchain (no JDK, no Gradle distribution extracted). Filed as ADFA-5037.

Test plan

  • Added AssetsInstallationHelperTest's `install reports Failure when doInstall's own preInstall catch block swallows an exception` — lets the real doInstall() run (unlike the existing `install with missing asset skips glitchtip` test, which mocks doInstall() itself and so never exercises this bug), stubbing only the underlying installer's preInstall to throw.
  • Confirmed the new test fails on dcda33cae (pre-fix) and passes with this change (:app:testV8DebugUnitTest --tests "com.itsaky.androidide.assets.AssetsInstallationHelperTest").
  • Full com.itsaky.androidide.assets.* package tests pass.

install() wrapped doInstall() in runCatching, but doInstall()'s
preInstall failure path caught FileNotFoundException/ZipException/
IOException and returned a Result.Failure value instead of throwing.
runCatching only detects thrown exceptions, so that returned Failure
was discarded and install() reported Success regardless.

Rethrow after logging/flashing in each catch block instead, so the
failure actually reaches install()'s exception handling.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a4125f29-0048-4efa-bc9c-5ab891932c8f

📥 Commits

Reviewing files that changed from the base of the PR and between 7e6cb30 and 776f446.

📒 Files selected for processing (4)
  • app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt
  • app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt
  • app/src/main/java/com/itsaky/androidide/assets/SplitAssetsInstaller.kt
  • app/src/main/java/com/itsaky/androidide/utils/TerminalInstallResultExtensions.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt
  • app/src/main/java/com/itsaky/androidide/assets/SplitAssetsInstaller.kt

📝 Walkthrough
  • AssetsInstallationHelper.install() now reports failures from doInstall().
  • Corrupt archives use missing-assets handling and suppress GlitchTip reporting.
  • Cleanup runs under NonCancellable, and cleanup failures do not replace the original exception.
  • SplitAssetsInstaller and BundledAssetsInstaller share terminal-result validation.
  • SplitAssetsInstaller now throws when bootstrap installation retries fail.
  • Added regression coverage for failure propagation and cleanup.
  • Risk: callers may now receive installation failures that were previously hidden. Callers must handle these failures.

Walkthrough

The asset installation flow now propagates corrupt-archive and installation failures with their causes. Cleanup runs after preInstall failures. Bundled and split installers now throw explicit exceptions for terminal failures. Tests verify failure details and cleanup.

Changes

Installation failure handling

Layer / File(s) Summary
Propagate helper failures and cleanup
app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt
install classifies corrupt ZIP archives as missing assets. doInstall rethrows preInstall exceptions and always attempts postInstall and staging-directory deletion.
Propagate terminal installer failures
app/src/main/java/com/itsaky/androidide/utils/TerminalInstallResultExtensions.kt, app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt, app/src/main/java/com/itsaky/androidide/assets/SplitAssetsInstaller.kt
throwIfNotSuccess maps terminal results to exceptions. Bundled and split installers use this validation. Split bootstrap failures throw an IOException after retry exhaustion.
Validate failure reporting and cleanup
app/src/test/java/com/itsaky/androidide/assets/AssetsInstallationHelperTest.kt
Tests verify preserved exception causes, skipped GlitchTip reporting, post-install execution, and staging-directory deletion.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant AssetsInstallationHelper
  participant preInstall
  participant postInstall
  participant StagingDirectory
  Caller->>AssetsInstallationHelper: install()
  AssetsInstallationHelper->>preInstall: prepare assets
  preInstall-->>AssetsInstallationHelper: throw installation exception
  AssetsInstallationHelper->>postInstall: attempt cleanup
  AssetsInstallationHelper->>StagingDirectory: delete staging directory
  AssetsInstallationHelper-->>Caller: return failure with original cause
Loading

Possibly related PRs

Suggested reviewers: daniel-adfa, hal-eisen-adfa, itsaky-adfa

Poem

A rabbit checks each asset trail,
Preserves the cause when installs fail.
Cleanup runs through every turn,
Terminal errors now return,
Tests confirm the paths prevail.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: preventing install() from swallowing doInstall() failures.
Description check ✅ Passed The description directly explains failure propagation, cleanup changes, shared result validation, and regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/ADFA-5037-install-swallows-failure

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt`:
- Around line 128-145: Restructure doInstall so ASSETS_INSTALLER.preInstall and
the subsequent installation work share the same outer try/finally block, with
cleanup always invoking postInstall and stagingDir.deleteRecursively(). Preserve
the existing exception logging, progress reporting, and rethrow behavior while
ensuring failures from preInstall also trigger symmetric installer and
staging-directory cleanup.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 292102f5-5ef7-4022-9941-ba904467ef79

📥 Commits

Reviewing files that changed from the base of the PR and between dcda33c and de3f817.

📒 Files selected for processing (2)
  • app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt
  • app/src/test/java/com/itsaky/androidide/assets/AssetsInstallationHelperTest.kt

Comment thread app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt Outdated
preInstall's try/catch was a separate block from the main install
work's try/finally, so a rethrown preInstall failure skipped
postInstall() (closing installer resources like SplitAssetsInstaller's
zipFile) and never deleted the staging directory. Merge both into one
outer try/finally so cleanup always runs.
The regression test only checked the returned Failure's cause chain,
not that postInstall() and staging-dir deletion actually still ran --
the specific behavior the previous commit added. Capture the staging
dir passed to preInstall, stub postInstall, and verify both.
- Move Brotli4jLoader.ensureAvailability() inside doInstall's try block
  so a load failure also gets the symmetric postInstall/staging-dir
  cleanup, not just preInstall failures.
- Extract the three preInstall catch blocks' log-then-rethrow into a
  logAndRethrow(prefix, e): Nothing helper, and drop the flashError/
  onProgress calls in them -- install()'s failure handling already
  notifies the user once (onProgress + ShowError event) with a better
  message; leaving those in doInstall meant the user saw two different
  flashbars for one failure now that the exception actually propagates.
- Wrap stagingDir.deleteRecursively() in the same runCatching pattern
  already used for postInstall, instead of Files.exists() (always true
  -- nothing between createTempDirectory and here ever removes it).
  A cleanup failure can no longer replace the real exception already
  propagating out of the try block.
- Rethrow CancellationException from postInstall's cleanup runCatching,
  consistent with install()'s own explicit handling.
- Broaden the "missing or corrupt assets" friendly-message check to
  also cover ZipException (corrupt archive), not just
  FileNotFoundException -- both are now reachable via the same
  preInstall catch blocks and share the same "reinstall" remedy.
- SplitAssetsInstaller: fix the sibling instance of the exact bug this
  ticket is about. The BOOTSTRAP_ENTRY_NAME branch logged and returned
  on retry failure / non-Success TerminalInstaller results instead of
  throwing, so that entry's async job reported FINISHED regardless.
  Mirrors BundledAssetsInstaller's equivalent branch, which already
  throws in both cases.

Test changes:
- Hoist the repeated checkStorageAccessibility stub into @before and
  the repeated Result.Failure cause-chain assertions into a shared
  assertMissingAssetFailure() helper.
- Add @after { unmockkAll() }, removing the class-wide mock leak (the
  @before mockkObject was never torn down) and the new test's manual
  per-mock unmock bookkeeping.
- Drop the flashError stub (no longer called) and a redundant
  isCaptured assertion (the following .captured access already fails
  loudly if nothing was captured).
- Documented (comment only) that the new test depends on
  AssetsInstaller.CURRENT_INSTALLER resolving to SplitAssetsInstaller,
  which holds for :app:testV8DebugUnitTest but not every build variant.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/src/test/java/com/itsaky/androidide/assets/AssetsInstallationHelperTest.kt (1)

19-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use JUnit Jupiter and Truth for this test class.

Replace the org.junit.* imports and JUnit 4 Assert.* assertions with JUnit Jupiter lifecycle annotations and Truth assertions for the new coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/test/java/com/itsaky/androidide/assets/AssetsInstallationHelperTest.kt`
at line 19, Update AssetsInstallationHelperTest to use JUnit Jupiter lifecycle
annotations instead of the org.junit imports, and replace all JUnit 4 Assert
assertions with Truth assertions. Keep the existing test behavior and coverage
unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@app/src/test/java/com/itsaky/androidide/assets/AssetsInstallationHelperTest.kt`:
- Line 19: Update AssetsInstallationHelperTest to use JUnit Jupiter lifecycle
annotations instead of the org.junit imports, and replace all JUnit 4 Assert
assertions with Truth assertions. Keep the existing test behavior and coverage
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 091f2215-8561-4b45-956b-f9c69ff72f8c

📥 Commits

Reviewing files that changed from the base of the PR and between 7c92e95 and 7e6cb30.

📒 Files selected for processing (3)
  • app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt
  • app/src/main/java/com/itsaky/androidide/assets/SplitAssetsInstaller.kt
  • app/src/test/java/com/itsaky/androidide/assets/AssetsInstallationHelperTest.kt

- Run postInstall() under withContext(NonCancellable) in doInstall's
  finally block. When one of the parallel installerJobs fails, the
  coroutineScope is already Cancelling by the time this finally runs,
  so postInstall()'s own withContext(Dispatchers.IO) would otherwise
  throw CancellationException at that suspension point before its
  body -- closing SplitAssetsInstaller.zipFile, running the chmod
  loop -- ever executes. Verified empirically with a standalone
  kotlinx-coroutines-core 1.10.2 repro: cleanup only actually ran with
  the NonCancellable wrap; the real underlying failure still
  propagates correctly either way.

- Extract the identical "throw if the terminal install result wasn't
  Success" when-block, now duplicated between SplitAssetsInstaller
  and BundledAssetsInstaller, into a shared
  TerminalInstaller.InstallResult.throwIfNotSuccess(context) extension
  (new file, to avoid pulling the large non-tab-formatted
  TerminalInstaller.kt under the Spotless ratchet for an unrelated
  reformat). Two copies of this exact logic were exactly how ADFA-5037
  happened in the first place.
@davidschachterADFA
davidschachterADFA merged commit a80a8fa into stage Aug 6, 2026
4 checks passed
@davidschachterADFA
davidschachterADFA deleted the bugfix/ADFA-5037-install-swallows-failure branch August 6, 2026 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants