Eric Seidel 96fd32796e feat: stage 2 integration tests — fake patch server + golden path + rollback regression (#355)
Lands the fake HTTP server, the patch fixture pipeline, and the first
two scenarios that exercise the real FFI path end-to-end. Plus a test
that would have caught the patch-to-release rollback bug
(shorebirdtech/shorebird#3728).

Architecture follows the principles surfaced in review:

- Dart tests call the public `ShorebirdUpdater` API only — never the
  raw `Updater` FFI wrapper, never the engine API.
- Engine API stays inside the `library_test_hooks` Rust crate.
  `shorebird_test_init` constructs `AppParameters` + stub
  `FileCallbacks` internally so Dart never sees those types.
  `shorebird_test_simulate_successful_launch` wraps the
  start/success protocol so the Dart layer never knows there's a
  protocol — it just knows "the engine reported a successful boot."
- ffigen scans only the test_hooks header. The engine header
  (updater_engine.h) does not appear in the Dart bindings.
- A `TestEngine` Dart helper concentrates engine-side simulation in
  one place; the test bodies stay focused on `ShorebirdUpdater`.

Implementation choices worth flagging:

- FFI calls run via `Isolate.run`. Synchronous run blocks the main
  isolate, deadlocking against the in-isolate shelf server. The
  test's IsolateRun callback re-opens the cdylib (cheap: dlopen is
  ref-counted) and resets `Updater.bindings` in the sub-isolate
  because Dart isolates do not share static fields.
- `libapp_path` must be a real file on the desktop integration build:
  the non-Android non-iOS non-test `patch_base` reads it directly
  from disk. Tests that install a patch write the fixture's `base`
  bytes to `libapp.so` before init.
- Fake server kept minimal: shelf, no Range support, no auth, no
  concurrency knobs. Stage 3+ scenarios (download cutoff, hash
  mismatch loop, etc.) extend it as needed.

Three scenarios cover three reasons we wanted this suite:

1. `checkForUpdate returns upToDate when server has no patch` —
   baseline: confirms the harness boots cleanly and returns the
   expected enum.
2. `install a patch and boot from it` — golden path: check →
   update → simulateSuccessfulLaunch, then assert
   `readCurrentPatch` / `readNextPatch` / `checkForUpdate`
   transitions match the public API contract.
3. `checkForUpdate returns restartRequired after patch-to-release
   rollback` — regression for shorebirdtech/shorebird#3728. Pre-fix
   this returned `upToDate` and left no signal to prompt a restart.

Verified locally: 232 Rust unit tests + 44 Dart tests (41 existing
unit + 3 new integration) green; clippy/fmt/cspell clean.
2026-05-06 08:11:47 -07:00
2024-04-19 13:51:22 -07:00

Updater library

codecov License: MIT License: Apache

This is the Rust side of the Shorebird code push system. This is built in Rust with a C API for easy calling from other languages, most notably for linking into libflutter.so.

The primary modification Shorebird makes to the stock Flutter engine is adding support for the updater library (this repo). The updater library is written in Rust and is used to update the code running in the Flutter app. The updater library is built as a static library and is linked into the Flutter engine during build time.

Parts

  • library: Runtime library linked into Flutter Engine to unpack and apply updates. Build into libupdater.a and linked into libflutter.so.
  • patch: Developer tooling to package Shorebird updates ("patches"). Built into patch.exe and downloaded and run by shorebird command line tool: https://github.com/shorebirdtech/shorebird/tree/main/packages/shorebird_cli.
  • shorebird_code_push: The Dart bindings for communicating with the updater library from within a Flutter app. Published to pub.dev, usage is optional by developers.

Most interesting code is in the library directory. There is also a README.md in that directory explaining the design.

Developing

It's best to edit this repository from within an engine checkout. See BUILDING_ENGINE.md for instructions on how to set up an engine checkout.

The workflow I use involves 2 to 3 VSC windows:

  1. Opening the engine src.

In that terminal I:

cd third_party/updater
  1. To build the updater as part of the engine:
cargo ndk --target aarch64-linux-android build --release && \
    ninja -C ../../out/android_release_arm64 && say "done"

The cargo part should not be needed, but I haven't yet done the work to integrate the Rust code into the gn files for the Flutter engine yet.

I add say "done" to the end as linking can take several minutes for release Android builds.

  1. In a second window, I open code third_party/updater. I do this because otherwise the rust_analyzer can't seem to find the rust code. We could fix this by adding the directory to the VSC workspace, but I'm not sure where we would put the workspace file in the first place. src is actually shorebirdtech/buildroot and is controlled via gclient by shorebirdtech/engine/DEPS.

  2. In a third window I open my test app. e.g.:

flutter create test_app
cd test_app
shorebird init
shorebird release
code .
  1. To run the test app with my local engine I use:
shorebird run --local-engine-src-path $HOME/Documents/GitHub/engine/src \
    --local-engine android_release_arm64

You may also need to build out/host_release once as flutter build looks for some Dart .dill files in host_release.

Coverage

We'd like to get to 100% coverage but aren't there yet.

https://github.com/taiki-e/cargo-llvm-cov is the best tool I've found for generating coverage reports.

Install: https://github.com/taiki-e/cargo-llvm-cov#installation

cargo llvm-cov will then generate the report.

S
Description
No description provided
Readme 1.6 MiB
Languages
Rust 46.3%
Dart 45.3%
C++ 3.2%
CMake 2.7%
C 1.6%
Other 0.9%