* docs: integration tests design proposal Adds docs/integration_tests.md proposing a desktop-only integration suite that drives the Dart `ShorebirdUpdater` API against the real Rust core via FFI, with a fake HTTP server and per-test tempdir state. Describes a new `library_test_hooks` cdylib that reaches into `updater` via a `test-hooks` Cargo feature, so production C API stays clean. Status: design exploration, not a committed plan. * feat: stage 1 integration test harness for shorebird_code_push Lands the test_hooks crate, the Dart-side loader, and one trivial end-to-end test, per docs/integration_tests.md stage 1. Pieces: - New `library_test_hooks` workspace crate (cdylib). Depends on `updater` with a new `test-hooks` Cargo feature that widens the visibility of internal items (currently `testing_reset_config`) for sibling-crate access only — production builds do not enable it. The crate also re-exports `updater::c_api::dart::*` and `updater::c_api::engine::*`, which keeps the rlib's `#[no_mangle]` symbols out of DCE so the resulting cdylib carries both the production C API and the new `shorebird_test_*` hooks. - Workspace `default-members` excludes `library_test_hooks` so plain `cargo build` / `cargo test` invocations don't unify the `test-hooks` feature into production builds. - `shorebird_code_push/test/integration/all_test.dart` (single file, with a header comment explaining why) loads the cdylib via `DynamicLibrary.open`, reassigns the existing `@visibleForTesting` `Updater.bindings` setter, and exercises both surfaces. The build helper shells out to `cargo build -p library_test_hooks`; if it fails, `markTestSkipped` keeps the suite green on machines without a working Rust toolchain. - ffigen config (`ffigen_test_hooks.yaml`) generates the test-only Dart bindings under `test/integration/generated/`, not `lib/`. - CI: `library_test_hooks` is added to the rust_crate matrix, and the shorebird_code_push job triggers on `library/**` and `library_test_hooks/**` so cdylib changes can't break the Dart-side integration suite without CI noticing. Verified locally: 232 Rust unit tests + 42 Dart tests (41 existing + 1 integration) green; clippy/fmt/cspell clean; production cdylib does not contain `shorebird_test_reset` (`nm` confirms feature isolation). Stages 2 (FakePatchServer + golden path) and 3 (adversarial scenarios) land separately. * fix(integration test): early-return on skip and bump per-test timeout Two issues caught by Shorebird CI on PR #353: 1. `markTestSkipped` does not abort test execution — it only flags the test as skipped on its way out. The body kept running and crashed on the `late testHooks` field when the cdylib build had failed in setUpAll. Move the skip check into the test body itself with an early return; drop the (no-op) skip in `setUp`. 2. Default per-test timeout (30s) also covers `setUpAll`. A cold `cargo build -p library_test_hooks` compiles `updater` and ~100 transitive deps, which can run minutes on CI. Bump to 10 minutes via `@Timeout` on the library. Verified locally: passes when cargo is on PATH (1 passed), reports a clean skip and exit 0 when cargo is removed from PATH (1 skipped). * refactor(integration test): drop ! by promoting testHooks to late final `testHooks` was nullable so accessing it after the skipReason check required `!`, and `markTestSkipped(skipReason!)` had the same smell. Make `testHooks` `late final` (non-nullable, throws if read before setUpAll assigns) and pull `skipReason` into a non-null local before use. Same control flow, no bang operators.
This Dart package communicates with the Shorebird Code Push Updater to:
- ✅ Get the currently installed patch version
- ✅ Check whether a new patch is available
- ✅ Download new patches
Demo
Explore this interactive demo to learn more
Getting Started
If your Flutter app does not already use Shorebird, follow our Getting Started Guide to add code push to your app.
Installation
flutter pub add shorebird_code_push
Usage
Shorebird automatically checks for and downloads updates in the background. Most apps do not need this package. This package is for apps that want additional control, such as displaying update status to the user or prompting before downloading.
Important: checkForUpdate() and update() make network calls that may
be slow. Avoid gating app startup on the result (e.g. awaiting in initState),
as the app may appear stuck on the splash screen. Use .then() instead.
import 'package:shorebird_code_push/shorebird_code_push.dart';
void main() => runApp(const MyApp());
// [Other code here]
class _MyHomePageState extends State<MyHomePage> {
final updater = ShorebirdUpdater();
Patch? _currentPatch;
bool _updateAvailable = false;
@override
void initState() {
super.initState();
// Read the current patch number (null if no patch is installed).
updater.readCurrentPatch().then((patch) {
setState(() => _currentPatch = patch);
});
// Check if an update is available to show in the UI.
updater.checkForUpdate().then((status) {
setState(() => _updateAvailable = status == UpdateStatus.outdated);
});
}
// [Other code here]
}
See the example for a complete working app.
Tracks
Shorebird supports publishing patches to different tracks, which can be used to target different segments of your user base. See the percentage based rollout guide for implementation details.
You must first publish a patch to a specific track (patches are published to the
stable track by default). To publish a patch to a different track, update your
patch command to use the --track argument:
shorebird patch android --track beta
(We're just using Android for this example. Tracks are supported on all platforms).
To check for updates on a given track, pass an UpdateTrack to
checkForUpdate (and update if you use it):
updater.checkForUpdate(track: UpdateTrack.beta);
You can also use custom track names. When creating a patch, specify a track name like this:
shorebird patch android --track my-custom-track
And:
updater.checkForUpdate(track: UpdateTrack('my-custom-track'));
Note: Updating to a specific track does not uninstall patches from other tracks. See #3484 for details.
Join us on Discord!
We have an active Discord server where you can ask questions and get help.
Contributing
See CONTRIBUTING.md.
