Files
shorebird-updater/shorebird_code_push/test/integration/helpers/fixtures.dart
T
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

75 lines
1.6 KiB
Dart

import 'dart:typed_data';
/// A precomputed bidiff patch artifact along with its hash, mirroring
/// the `PatchFixture` constants in `library/src/c_api/mod.rs`. The
/// bytes were generated with:
///
/// cargo run --bin string_patch -- "<base>" "<new>"
///
/// They are self-contained zstd payloads — applying them against an
/// empty source produces the `new` content, so tests using the stub
/// `FileCallbacks` from `shorebird_test_init` (which read 0 bytes
/// from the source apk) still get the right inflated bytes.
class PatchFixture {
PatchFixture({
required this.number,
required this.base,
required this.newContent,
required this.hash,
required this.bytes,
});
final int number;
/// The libapp bytes the patch was generated against. The integration
/// harness writes these to `libapp_path` before init — on non-test
/// desktop builds, `patch_base` reads that file directly when
/// applying the patch.
final Uint8List base;
final String newContent;
final String hash;
final Uint8List bytes;
}
/// `string_patch "hello world" "hello tests"`.
final helloTestsPatch = PatchFixture(
number: 1,
base: Uint8List.fromList('hello world'.codeUnits),
newContent: 'hello tests',
hash: 'bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45',
bytes: Uint8List.fromList(const [
40,
181,
47,
253,
0,
128,
177,
0,
0,
223,
177,
0,
0,
0,
16,
0,
0,
6,
0,
0,
0,
0,
0,
0,
5,
116,
101,
115,
116,
115,
0,
]),
);