The Dart suite spent 77% of its cost compiling one isolate per test file while `flutter test` used half the cores, and every Maestro flow replayed a full Jellyfin onboarding before its first real assertion. - Add scripts/run_tests.sh, which runs `flutter test` with -j set to the cores the process may actually use instead of the ncpu/2 default. Measured on 8 cores: 190s -> 136s; -j 12 regresses to 165s, so it scales to the core count rather than hard-coding one. CI and CONTRIBUTING use it. A cgroup v2 quota, a cgroup v1 quota, and the cpuset/affinity nproc reports can each be the binding limit independently, so the detector takes the smallest; trusting whichever it found first would oversubscribe 4x on a container holding an 8-CPU quota while pinned to 2. Covered by scripts/test_run_tests.py, which the ci_guard_checks.sh glob picks up. - Add .maestro/subflows/ensure_onboarded.yaml: cold-start the app and only onboard when no session is stored. Flows that just need a signed-in Home use it; 02_onboarding_home, 08_logout, 09_download_offline_playback and the profile regressions keep clearing state. 59s -> 16s per flow. - Guard onboarding's two optional taps behind visibility checks. A missed `optional: true` tap still runs the full element search, costing 3.0s and 7.8s per onboarding to find nothing. - Disable device animation scales in run_maestro.py, restored by the existing cleanup path. CI's emulator got this from the runner flag; physical devices never did. - Shorten the watch_together setup-timeout replacement from 500ms to the 10ms the same file already proves sufficient, and shorten the retry backoff at the one site that missed it: 8.04s -> 1.59s of execution. - Make the LAN discovery waits deadline-based and resend the beacon while polling. Loopback UDP drops datagrams under load, which timed out a wait that could never be satisfied; this was the suite's one flaky test. - Fix 08_logout, which searched for "Logout" and "Are you sure you want to logout?" after both strings became "Log out". The flow had been failing and aborting the suite before 09 ever ran. flutter test 190s -> 131s. Maestro's Android suite 621s -> 385s across the eight flows the baseline reached, and now runs all nine green.
6.6 KiB
Contributing
Getting Started
- Fork and clone the repository
- Run
flutter pub getto install dependencies - Run
scripts/codegen.shto generate translations and Dart model code - Start developing!
Development
- Follow Dart/Flutter conventions
- Run
dart format .to format Dart code (note: generated files like*.g.dartare excluded from CI checks) - Run
scripts/format_native.sh --fixto format Kotlin, Swift, C++, C, Objective-C, and native headers - Run
flutter analyzebefore submitting to check for issues - Run
scripts/run_tests.shto run the test suite (same asflutter test, but scaled to your core count) - Test your changes thoroughly
Code Quality Checks
The project includes automated CI checks that run on all pull requests:
-
Code Formatting: Ensures code follows Dart and native formatting standards
- Run locally:
dart format .to format Dart files - Run locally:
scripts/format_native.sh --fixto format native files - Note: CI only checks non-generated files (excludes
.g.dart,.freezed.dart) - Generated files are reformatted automatically by build tools
- Run locally:
-
Static Analysis: Checks for code issues and potential bugs
- Run locally:
flutter analyze - Note: CI excludes generated files from analysis (configured in
analysis_options.yaml)
- Run locally:
-
Generated Code: Ensures generated translations and model files are current
- Run locally:
scripts/codegen.sh --check
- Run locally:
-
Tests: Runs unit and widget tests (when available)
- Run locally:
scripts/run_tests.sh - This is
flutter testwith-jset to the core count. The default is half your cores, which leaves most of the machine idle because the suite is dominated by per-file compilation. Arguments are forwarded, soscripts/run_tests.sh test/widgets/some_test.dartworks.
- Run locally:
All these checks must pass before your changes can be merged.
Maestro end-to-end tests
Android E2E tests use Maestro against a disposable, pre-seeded Jellyfin container.
Prerequisites: Java 17, Flutter and Android SDK/platform tools, a running Android emulator, Docker, and the Maestro CLI.
Run the suites from the repository root (py -3 can replace python3 on Windows):
python3 scripts/run_maestro.py basic # Basic user flows
python3 scripts/run_maestro.py catalog # Catalog and music flows
python3 scripts/run_maestro.py media # Codec playback and track selection
Run one flow with --flow:
python3 scripts/run_maestro.py basic --flow .maestro/flows/04_search.yaml
Use --skip-build to reuse the debug APK and --skip-jellyfin-build to reuse the Jellyfin image. Set
--device <adb-serial> when multiple devices are connected; physical devices also require --adb-reverse.
Top-level flows live in .maestro/flows/, shared setup in .maestro/subflows/, and focused regressions in
.maestro/regression_flows/. Automatic PR groups are declared in scripts/run_maestro_ci.py::GROUPS. Every top-level
regression flow must be registered either there or in DESTRUCTIVE_MANUAL_TARGETS; reusable subflows are not
independent tests. A manual-only classification must state why the flow cannot run automatically and must not be
described as CI coverage. .github/workflows/e2e.yml runs only automatic targets and uploads diagnostics on failure.
The profile-isolation and profile-teardown regressions create and remove profile connections, so they are a destructive manual target rather than automatic PR coverage. Run them only against the pre-seeded Jellyfin fixture and a disposable emulator, using the required opt-in:
python3 scripts/run_maestro_ci.py profile-regressions --disposable-emulator
The target refuses to start without --disposable-emulator. Each profile flow writes to its own Jellyfin log and
diagnostics directory under build/maestro-profile-regressions/.
Production container image updates
Production images in server/Dockerfile and server/docker-compose.yml use a readable version or source-revision tag
plus an authoritative multi-platform index digest. The adjacent Platforms declaration records the supported
linux/amd64 and linux/arm64 variants. Never replace these references with a mutable tag or a single-platform child
manifest.
Update a production image only through a reviewed change:
- For the Bugs service, first record the running container's image ID, repository digest, platform, and OCI source revision without printing its environment. Prefer that reviewed running identity; selecting anything else is a service upgrade, not a routine pin refresh.
- Review the upstream source revision and changelog, provenance, vulnerability results, and manifest contents. Resolve the readable tag and digest-qualified reference independently and confirm they identify the same OCI index in two clean caches. The index must contain both declared platforms; provenance/attestation descriptors do not count as runnable platforms.
- Change the readable tag, full
sha256index digest, and adjacent platform declaration together. Include the old and new identities, manifest/platform evidence, review findings, smoke results, and rollback notes in the change. - Before changing the Bugs digest, exercise it with non-production configuration and a disposable volume. Review
migrations, take a restorable
bugs_databackup, then validate a cloned volume. A forward-only migration rolls back with the prior digest and pre-change backup, not by changing the image reference alone. - Run
python3 scripts/check_container_image_pins.py,python3 scripts/test_check_container_image_pins.py, and(cd server && go test ./...). Inspect the rendered Compose configuration and rebuilt images locally without exposing configuration values. Do not publish or deploy from a review checkout, and never fall back tolatestwhen a digest is unavailable.
Internationalization (i18n)
This project uses slang for internationalization with JSON files.
Adding New Strings
-
Add your string to
lib/i18n/strings.i18n.json:{ "section": { "myNewString": "My new text" } } -
Run
dart run slangto regenerate translation files -
Use in your code:
Text(t.section.myNewString)
Adding New Languages
- Create new JSON file:
lib/i18n/[locale].i18n.json - Copy structure from
en.i18n.jsonand translate values - Run
dart run slangto regenerate files
Guidelines
- Organize strings logically in nested objects
- Use camelCase for keys
- Keep strings concise and clear
- Always run
dart run slangafter changes