fix: Android E2E aliceTile race + bundle deploy-android infra

The Android UI integration test failed at tap(aliceTile) with "0 widgets"
even though pumpUntil had just found the tile. On the slow software-rendered
emulator the route-pop animation finalises during pumpUntil's trailing 300 ms
settle, briefly leaving the tile out of the tree. Re-confirm with a second
pumpUntil before the tap.

Bundles the previously uncommitted infra changes that make task deploy-android
run end-to-end inside nix develop: Linux desktop runtime libs + GL software
rendering env in flake.nix, path_provider_android pin to <2.3 to avoid the
libdartjni SIGSEGV, deferred DB-path resolution after WidgetsFlutterBinding,
+iglx for xvfb-run, platform-tools on PATH, and a single pre-commit script
replacing the dart-format / task-check-fast pair.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thomas Güttler
2026-04-28 12:36:30 +02:00
co-authored by Claude Opus 4.7
parent d2226388d7
commit 2e2b7c3d9f
14 changed files with 150 additions and 36 deletions
+2
View File
@@ -12,5 +12,7 @@ use flake
# This must come after `use flake` so FVM Flutter takes precedence. # This must come after `use flake` so FVM Flutter takes precedence.
PATH_add .fvm/flutter_sdk/bin PATH_add .fvm/flutter_sdk/bin
PATH_add "$HOME/Android/Sdk/platform-tools"
# Load variables from .env # Load variables from .env
dotenv_if_exists dotenv_if_exists
+3 -9
View File
@@ -1,15 +1,9 @@
repos: repos:
- repo: local - repo: local
hooks: hooks:
- id: dart-format - id: dart-check
name: dart format name: dart format (autofix) + check-fast (parallel)
language: system language: system
entry: bash -c 'cd "$(git rev-parse --show-toplevel)" && nix develop --command fvm dart format .' entry: bash -c 'cd "$(git rev-parse --show-toplevel)" && nix develop --command scripts/pre_commit_check.sh'
pass_filenames: false
always_run: true
- id: task-check
name: task check-fast (analyze + unit + widget)
language: system
entry: bash -c 'cd "$(git rev-parse --show-toplevel)" && nix develop --command task check-fast'
pass_filenames: false pass_filenames: false
always_run: true always_run: true
+1 -1
View File
@@ -35,7 +35,7 @@ tasks:
preconditions: preconditions:
- sh: command -v clang >/dev/null 2>&1 - sh: command -v clang >/dev/null 2>&1
msg: "Linux desktop deps missing. Run: sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev" msg: "Linux desktop deps missing. Run: sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev"
- sh: /usr/bin/pkg-config --exists gtk+-3.0 2>/dev/null - sh: pkg-config --exists gtk+-3.0 2>/dev/null
msg: "Linux desktop deps missing. Run: sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev" msg: "Linux desktop deps missing. Run: sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev"
install-hooks: install-hooks:
+1 -7
View File
@@ -47,10 +47,4 @@ flutter {
source = "../.." source = "../.."
} }
dependencies { dependencies {}
// integration_test is a dev dependency; the Flutter plugin loader adds it as
// debugImplementation only, but GeneratedPluginRegistrant.java (in src/main)
// references its class in all variants. Make it available for release compilation
// without bundling it in the APK.
releaseCompileOnly(project(":integration_test"))
}
@@ -0,0 +1,19 @@
package dev.flutter.plugins.integration_test;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
/**
* No-op stub used in release builds.
* The real IntegrationTestPlugin lives in the integration_test SDK package
* which is a dev-only dependency. GeneratedPluginRegistrant references it in
* all build variants, so without this stub the release build throws
* NoClassDefFoundError at startup, aborting plugin registration entirely.
*/
public class IntegrationTestPlugin implements FlutterPlugin {
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {}
}
+35
View File
@@ -6,6 +6,41 @@ Tasks get moved from next.md to done.md
## Tasks ## Tasks
## task deploy-android works end-to-end
The original "Emulator did not become ready within 120 s" was already resolved in
commit `d222638` by running `adb start-server` before booting the AVD; without the
adb daemon running first, the emulator can never register as a device.
Running `task deploy-android` after that surfaced an Android-specific integration-test
failure: `aliceTile` had 0 widgets at `tester.tap()` time even though the immediately
preceding `pumpUntil(aliceTile)` had just found it. On the slow software-rendered
emulator the route-pop animation finalises during `pumpUntil`'s trailing 300 ms settle
and the tile is briefly absent right after. Fixed in
`integration_test/app_e2e_test.dart` by re-confirming `aliceTile` with a second
`pumpUntil` (5 s timeout) before the tap.
Bundled with a coherent set of pre-existing infrastructure changes that make the full
pipeline (Linux + Android UI tests, MobSF scan, APK upload) work in `nix develop`:
- `flake.nix`: adds Linux desktop runtime libs (gtk3, mesa, libGL, libsecret, …) plus
`PKG_CONFIG_PATH`, `LD_LIBRARY_PATH`, `LIBGL_ALWAYS_SOFTWARE=1`, and the libglvnd
vendor-dir env vars so `flutter build linux` and `xvfb-run` work without a real GPU.
- `pubspec.yaml`: pins `path_provider_android` to `>=2.2.0 <2.3.0` to dodge the SIGSEGV
in `libdartjni.so` (FindClassUnchecked) on Android startup with 2.3+.
- `lib/main.dart` + `lib/data/db/database.dart`: resolves the DB path during `main()`
after `WidgetsFlutterBinding.ensureInitialized()` so the path_provider plugin channel
is registered before the first DB access.
- `stalwart-dev/integration_ui_test.sh`: passes `-screen 0 1280x720x24 +iglx` to Xvfb
so GTK3/Flutter can create a GLX OpenGL context under the virtual framebuffer.
- `.envrc`: adds `$HOME/Android/Sdk/platform-tools` to PATH so `adb` resolves outside
`nix develop`.
- `Taskfile.yml`: drops the `/usr/bin/pkg-config` hardcode in favour of PATH so the
nix-provided wrapper is found.
- `.pre-commit-config.yaml` + `scripts/pre_commit_check.sh`: consolidates `dart format`
and `task check-fast` into a single script invoked by one hook (one `nix develop`
startup instead of two).
## Replace custom search TextField with Flutter SearchBar ## Replace custom search TextField with Flutter SearchBar
Replaced the hand-rolled `TextField`-in-`AppBar` search UI with Flutter's built-in `SearchBar` Replaced the hand-rolled `TextField`-in-`AppBar` search UI with Flutter's built-in `SearchBar`
+46 -1
View File
@@ -10,6 +10,24 @@
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
let let
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
# All Linux desktop runtime libraries needed by flutter build linux and
# the UI integration tests (xvfb-run). Kept as a list so we can reuse
# it for both buildInputs and LD_LIBRARY_PATH / PKG_CONFIG_PATH.
linuxDesktopLibs = with pkgs; [
gtk3
libsecret
fontconfig
libepoxy
mesa
libGL # libglvnd — vendor-neutral GL/EGL/GLX dispatch layer
at-spi2-core
glib
pango
cairo
gdk-pixbuf
harfbuzz
];
in { in {
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
@@ -22,6 +40,13 @@
# Flutter version manager — needed for host builds (task build-linux, task run) # Flutter version manager — needed for host builds (task build-linux, task run)
fvm fvm
# Linux desktop build + runtime dependencies (flutter build linux / task run)
] ++ linuxDesktopLibs ++ (with pkgs; [
pkg-config
clang
cmake
ninja
# Local IMAP/SMTP dev server for integration tests # Local IMAP/SMTP dev server for integration tests
stalwart-mail stalwart-mail
@@ -37,12 +62,32 @@
jq jq
sqlite sqlite
python3 # used by stalwart-dev/start to pick random ports python3 # used by stalwart-dev/start to pick random ports
]; ]);
shellHook = '' shellHook = ''
# Disable Flutter telemetry inside dev shell # Disable Flutter telemetry inside dev shell
export FLUTTER_SUPPRESS_ANALYTICS=true export FLUTTER_SUPPRESS_ANALYTICS=true
# Expose dev headers to cmake's FindPkgConfig.
# The nix pkg-config wrapper works in bash but cmake invokes pkg-config
# as a subprocess and needs PKG_CONFIG_PATH set explicitly.
export PKG_CONFIG_PATH="${pkgs.gtk3.dev}/lib/pkgconfig:${pkgs.glib.dev}/lib/pkgconfig:${pkgs.pango.dev}/lib/pkgconfig:${pkgs.cairo.dev}/lib/pkgconfig:${pkgs.gdk-pixbuf.dev}/lib/pkgconfig:${pkgs.at-spi2-core.dev}/lib/pkgconfig:${pkgs.harfbuzz.dev}/lib/pkgconfig:${pkgs.libsecret}/lib/pkgconfig:${pkgs.fontconfig.dev}/lib/pkgconfig:${pkgs.libepoxy}/lib/pkgconfig:$PKG_CONFIG_PATH"
# Nix ld uses --no-copy-dt-needed-entries (strict mode): transitive shared-lib
# deps are not followed automatically, so link them explicitly.
export LDFLAGS="-L${pkgs.fontconfig.lib}/lib -lfontconfig $LDFLAGS"
# Make nix-built runtime libs visible to the dynamic linker so the
# Flutter Linux bundle and integration-ui tests can run.
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath linuxDesktopLibs}:$LD_LIBRARY_PATH"
# Wire the libglvnd dispatch to the nix mesa vendor ICDs so GTK/Flutter
# can create an OpenGL (EGL + GLX) context under Xvfb without a real GPU.
export __EGL_VENDOR_LIBRARY_DIRS="${pkgs.mesa}/share/glvnd/egl_vendor.d"
export __GLX_VENDOR_LIBRARY_DIRS="${pkgs.mesa}/lib"
export LIBGL_ALWAYS_SOFTWARE=1
export MESA_LOADER_DRIVER_OVERRIDE=softpipe
echo "SharedInbox Flutter dev environment ready." echo "SharedInbox Flutter dev environment ready."
echo " Analyze : task analyze" echo " Analyze : task analyze"
echo " Unit tests : task test" echo " Unit tests : task test"
+4
View File
@@ -220,6 +220,10 @@ void main() {
// ── Navigate to mailboxes ────────────────────────────────────────────── // ── Navigate to mailboxes ──────────────────────────────────────────────
_log('navigate to mailboxes'); _log('navigate to mailboxes');
// On the slow Android emulator (software rendering), aliceTile can be
// briefly absent right after pumpUntil's trailing 300ms settle while the
// route-pop animation finalises. Re-confirm it's present before tapping.
await pumpUntil(tester, aliceTile, timeout: const Duration(seconds: 5));
await tester.tap(aliceTile); await tester.tap(aliceTile);
await pumpUntil(tester, find.text('INBOX')); await pumpUntil(tester, find.text('INBOX'));
_log('mailboxes settled'); _log('mailboxes settled');
+17 -2
View File
@@ -251,10 +251,25 @@ class AppDatabase extends _$AppDatabase {
); );
} }
// Resolved once in main() via initDatabasePath() before runApp().
String? _dbPath;
/// Call after WidgetsFlutterBinding.ensureInitialized() so that the
/// path_provider plugin channel is registered before the first DB access.
Future<void> initDatabasePath() async {
final dir = await getApplicationSupportDirectory();
_dbPath = p.join(dir.path, 'sharedinbox.db');
}
LazyDatabase _openConnection() { LazyDatabase _openConnection() {
return LazyDatabase(() async { return LazyDatabase(() async {
final dir = await getApplicationSupportDirectory(); final file = File(
final file = File(p.join(dir.path, 'sharedinbox.db')); _dbPath ??
p.join(
(await getApplicationSupportDirectory()).path,
'sharedinbox.db',
),
);
return NativeDatabase.createInBackground( return NativeDatabase.createInBackground(
file, file,
setup: (db) { setup: (db) {
+4 -1
View File
@@ -1,10 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sharedinbox/data/db/database.dart';
import 'package:sharedinbox/di.dart'; import 'package:sharedinbox/di.dart';
import 'package:sharedinbox/ui/router.dart'; import 'package:sharedinbox/ui/router.dart';
void main({List<Override> overrides = const []}) { void main({List<Override> overrides = const []}) async {
WidgetsFlutterBinding.ensureInitialized();
await initDatabasePath();
runApp(ProviderScope(overrides: overrides, child: const SharedInboxApp())); runApp(ProviderScope(overrides: overrides, child: const SharedInboxApp()));
} }
-14
View File
@@ -17,17 +17,3 @@ Git repo should not contain unknown files.
Then commit. Then commit.
## Tasks ## Tasks
fix:
[6ms] no emulator running — booting AVD sharedinbox_test
Emulator did not become ready within 120 s
task: Failed to run task "deploy-android": task: Failed to run task "integration-android": exit status 1
Why does the emulator not start? Where are the logs?
After that: task deploy-android
fix, if it fails.
---
+6
View File
@@ -54,3 +54,9 @@ dev_dependencies:
flutter: flutter:
uses-material-design: true uses-material-design: true
dependency_overrides:
# path_provider_android 2.3+ uses package:jni which crashes on startup
# (SIGSEGV in libdartjni.so FindClassUnchecked — JNI env not ready when
# the Dart VM first calls into it). Pin to 2.2.x which uses Pigeon instead.
path_provider_android: ">=2.2.0 <2.3.0"
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Single nix develop session: format first (autofix), then check-fast.
# Flutter's startup lock prevents true parallelism between dart format and flutter analyze.
set -uo pipefail
cd "$(git rev-parse --show-toplevel)" || exit 1
fvm dart format .
task check-fast
+4 -1
View File
@@ -94,6 +94,9 @@ ts "flutter test start"
# xvfb-run provides a virtual framebuffer so the Flutter Linux runner has a # xvfb-run provides a virtual framebuffer so the Flutter Linux runner has a
# display without requiring a real desktop session. No D-Bus or keyring daemon # display without requiring a real desktop session. No D-Bus or keyring daemon
# is needed because the integration tests inject an in-memory SecureStorage. # is needed because the integration tests inject an in-memory SecureStorage.
xvfb-run --auto-servernum fvm flutter test integration_test/ -d linux # +iglx enables indirect GLX on Xvfb so Flutter/GTK3 can create an OpenGL context
# using mesa's software renderer (LIBGL_ALWAYS_SOFTWARE=1 is set in flake.nix).
xvfb-run --auto-servernum --server-args="-screen 0 1280x720x24 +iglx" \
fvm flutter test integration_test/ -d linux
ts "flutter test done" ts "flutter test done"