Skip to content

Commit 9e99fa5

Browse files
cynarlabclaude
andcommitted
test(dart): skip the Flutter desktop cases where no desktop can run; harden the Flutter e2e helpers (#790)
The first `flutter-host` run on ubuntu passed both widget-test cases and ran both desktop cases: `flutter devices` lists the host desktop whenever the platform feature flag is on (linux_workflow.dart `canListDevices`), display or toolchain or not, and `flutter run -d linux` died on the headless runner. From the scoped review of #881: - `flutterDesktopDeviceId()` answers null on Linux without a DISPLAY/WAYLAND_DISPLAY and anywhere with MCP_SKIP_FLUTTER_DESKTOP=1; the lane sets the variable and also filters the run to `-t 'widget test'`, so its name is true by construction. - `flutterDeviceIds()` parses the tool's stdout only (its warnings follow the JSON on stderr and used to turn the list into [] silently). - `which flutter` is realpath'd before deriving the root (Homebrew, asdf and fvm put symlinks on PATH); the tool-snapshot requirement applies on Windows only, where the adapter spawns the snapshot itself. - The example's docs and analysis options no longer assume Windows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 6ac0244 commit 9e99fa5

8 files changed

Lines changed: 54 additions & 21 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,8 +506,12 @@ jobs:
506506
- name: Build project
507507
run: pnpm build
508508
- name: Run Flutter host smoke (widget tests)
509+
# `flutter devices` lists `linux` on this headless runner too; the desktop cases need a
510+
# display and the GTK toolchain, so they are skipped explicitly (first run: both failed).
511+
env:
512+
MCP_SKIP_FLUTTER_DESKTOP: '1'
509513
run: |
510-
pnpm exec vitest run --project e2e tests/e2e/mcp-server-smoke-flutter.test.ts --reporter=default --reporter=json --outputFile=artifacts/flutter/host-tests.json
514+
pnpm exec vitest run --project e2e tests/e2e/mcp-server-smoke-flutter.test.ts -t 'widget test' --reporter=default --reporter=json --outputFile=artifacts/flutter/host-tests.json
511515
node scripts/check-flutter-e2e-report.mjs artifacts/flutter/host-tests.json
512516
- name: Upload Flutter validation evidence
513517
if: always()

‎examples/dart/README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ adapter (`dart debug_adapter`), which ships with every Dart and Flutter install.
1010
(Windows: `winget install Google.DartSDK`; macOS: `brew install dart`; Linux: the `dart` apt package).
1111
- For `dart_probe/`, resolve its one dev dependency once: `cd dart_probe && dart pub get`.
1212
- For `flutter_probe/`, a Flutter SDK with a warm tool cache (`flutter --version` once), then
13-
`cd flutter_probe && flutter create --platforms=windows,web . && flutter pub get` (the e2e helper
14-
does this itself); `flutter run` on the desktop also needs that platform's toolchain
15-
(`flutter doctor`).
13+
`cd flutter_probe && flutter create --platforms=<windows|macos|linux>,web . && flutter pub get`
14+
(the e2e helper does this itself for the host's desktop); `flutter run` on the desktop also needs
15+
that platform's toolchain (`flutter doctor`).
1616

1717
Do not run `dart test` from a project under `%LOCALAPPDATA%\Temp` with the winget SDK: package:test
1818
spawns the frontend server by a relative path that does not resolve from there.

‎examples/dart/flutter_probe/.gitignore‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by `flutter create --platforms=windows,web .` (the e2e helper runs it) and by builds.
1+
# Generated by `flutter create --platforms=<desktop>,web .` (the e2e helper runs it) and by builds.
22
.dart_tool/
33
.flutter-plugins-dependencies
44
.metadata

‎examples/dart/flutter_probe/README.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ The Flutter example the adapter's tests and docs use: a counter app (`lib/main.d
44
test (`test/widget_test.dart`) and an integration test (`integration_test/app_test.dart`).
55

66
Only the Dart sources and `pubspec.yaml` are committed. The platform folders the Flutter tool needs
7-
to run the app are generated, once, with:
7+
to run the app are generated, once, for your desktop (`windows`, `macos` or `linux`) and the web:
88

99
```bash
10-
flutter create --platforms=windows,web .
10+
flutter create --platforms=windows,web . # or macos,web / linux,web
1111
flutter pub get
1212
```
1313

14-
The e2e helper (`tests/e2e/dart-example-utils.ts`, `prepareFlutterProbe`) runs exactly that.
14+
The e2e helper (`tests/e2e/dart-example-utils.ts`, `prepareFlutterProbe`) runs exactly that for
15+
the host's desktop. `flutter create .` leaves the committed files alone (it never overwrites
16+
without `--overwrite`).
1517

1618
| File | Breakpoint markers |
1719
|---|---|
@@ -22,6 +24,6 @@ The e2e helper (`tests/e2e/dart-example-utils.ts`, `prepareFlutterProbe`) runs e
2224
```bash
2325
flutter test # the widget tests
2426
flutter test --name increments # one of them
25-
flutter run -d windows # the app on the desktop
27+
flutter run -d windows # the app on the desktop (-d macos / -d linux)
2628
flutter test integration_test -d windows # the integration test on the desktop
2729
```

‎examples/dart/flutter_probe/analysis_options.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ analyzer:
44
exclude:
55
- build/**
66
- android/**
7+
- ios/**
8+
- linux/**
9+
- macos/**
710
- web/**
811
- windows/**

‎tests/e2e/README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,9 @@ This directory contains end-to-end smoke tests that verify the MCP debugger serv
8080
- `mcp-server-smoke-flutter.test.ts` covers the Flutter runners against `examples/dart/flutter_probe`
8181
(`flutter debug-adapter --test` for the widget test, with and without `stopOnEntry`; `flutter run`
8282
and an `integration_test` on the desktop device `flutter devices` lists). It self-skips without a
83-
Flutter SDK with a warm tool cache, and the desktop cases skip without a desktop device (CI's
84-
`flutter-host` lane runs the widget-test cases only; `scripts/check-flutter-e2e-report.mjs`
83+
Flutter SDK with a warm tool cache, and the desktop cases skip without a desktop device — on
84+
Linux also without a `DISPLAY`, and anywhere with `MCP_SKIP_FLUTTER_DESKTOP=1` (CI's
85+
`flutter-host` lane sets it and runs the widget-test cases only; `scripts/check-flutter-e2e-report.mjs`
8586
requires them). The helper generates the probe's platform folders with `flutter create` on
8687
first use
8788

‎tests/e2e/dart-example-utils.ts‎

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* and locate `// BP-NAME` marker lines so moving a line never breaks an assertion.
55
*/
66
import { spawnSync } from 'child_process';
7-
import { existsSync, readFileSync, readdirSync } from 'fs';
7+
import { existsSync, readFileSync, readdirSync, realpathSync } from 'fs';
88
import path from 'path';
99
import { fileURLToPath } from 'url';
1010
import which from 'which';
@@ -91,7 +91,12 @@ function flutterRootCandidates(): string[] {
9191
const env = process.env;
9292
for (const key of ['FLUTTER_ROOT', 'FLUTTER_PATH']) if (env[key]) out.push(env[key]!);
9393
const onPath = which.sync('flutter', { nothrow: true });
94-
if (onPath) out.push(path.resolve(path.dirname(onPath), '..'));
94+
if (onPath) {
95+
// Package managers (Homebrew, asdf, fvm) put a symlink on PATH; the checkout is where it points.
96+
let real = onPath;
97+
try { real = realpathSync(onPath); } catch { /* keep the symlink path */ }
98+
out.push(path.resolve(path.dirname(real), '..'));
99+
}
95100
if (process.platform === 'win32') {
96101
out.push('C:\\src\\flutter', 'C:\\flutter', path.join(env.LOCALAPPDATA ?? '', 'flutter'));
97102
} else {
@@ -102,11 +107,15 @@ function flutterRootCandidates(): string[] {
102107

103108
let cachedFlutterRoot: string | null | undefined;
104109

105-
/** A Flutter checkout with a warm tool cache (the snapshot the adapter spawns on Windows); null when none. */
110+
/**
111+
* A Flutter checkout; on Windows one with a warm tool cache (the adapter spawns the tool snapshot
112+
* there instead of flutter.bat), elsewhere bin/flutter builds the cache itself. Null when none.
113+
*/
106114
export function findFlutterRootSync(): string | null {
107115
if (cachedFlutterRoot !== undefined) return cachedFlutterRoot;
108116
cachedFlutterRoot = flutterRootCandidates().find((root) =>
109-
root && existsSync(path.join(root, 'bin', FLUTTER_LAUNCHER)) && existsSync(path.join(root, 'bin', 'cache', 'flutter_tools.snapshot'))
117+
root && existsSync(path.join(root, 'bin', FLUTTER_LAUNCHER))
118+
&& (process.platform !== 'win32' || existsSync(path.join(root, 'bin', 'cache', 'flutter_tools.snapshot')))
110119
) ?? null;
111120
return cachedFlutterRoot;
112121
}
@@ -129,10 +138,11 @@ export function flutterToolArgv(root: string, args: string[]): { command: string
129138
};
130139
}
131140

132-
function runFlutter(root: string, args: string[], cwd: string, timeoutMs = 300_000): { status: number | null; out: string } {
141+
function runFlutter(root: string, args: string[], cwd: string, timeoutMs = 300_000): { status: number | null; stdout: string; out: string } {
133142
const argv = flutterToolArgv(root, args);
134143
const r = spawnSync(argv.command, argv.args, { cwd, encoding: 'utf8', env: argv.env, windowsHide: true, timeout: timeoutMs });
135-
return { status: r.status, out: `${r.stdout ?? ''}${r.stderr ?? ''}` };
144+
const stdout = r.stdout ?? '';
145+
return { status: r.status, stdout, out: `${stdout}${r.stderr ?? ''}` };
136146
}
137147

138148
let flutterProbePrepared = false;
@@ -167,17 +177,26 @@ export function flutterDeviceIds(): string[] {
167177
if (!root) return (cachedDevices = []);
168178
const r = runFlutter(root, ['devices', '--machine'], DART_EXAMPLES_DIR, 120_000);
169179
try {
170-
const start = r.out.indexOf('[');
171-
const list = JSON.parse(r.out.slice(start)) as Array<{ id?: string }>;
180+
// stdout only: the tool's warnings (`[!] adb …`, startup-lock notices) go to stderr and would
181+
// follow the array.
182+
const start = r.stdout.indexOf('[');
183+
const list = JSON.parse(r.stdout.slice(start)) as Array<{ id?: string }>;
172184
cachedDevices = list.map((d) => d.id).filter((id): id is string => typeof id === 'string');
173185
} catch {
174186
cachedDevices = [];
175187
}
176188
return cachedDevices;
177189
}
178190

179-
/** The desktop device id for this OS, when `flutter devices` lists it. */
191+
/**
192+
* The desktop device id for this OS, when `flutter devices` lists it and it can actually run an
193+
* app: `flutter devices` lists `linux` on a headless runner too, where `flutter run -d linux`
194+
* dies for want of a display and the GTK toolchain (measured on CI's ubuntu lane), so Linux needs
195+
* a DISPLAY/WAYLAND_DISPLAY; `MCP_SKIP_FLUTTER_DESKTOP=1` skips the desktop cases anywhere.
196+
*/
180197
export function flutterDesktopDeviceId(): string | null {
198+
if (process.env.MCP_SKIP_FLUTTER_DESKTOP === '1') return null;
199+
if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return null;
181200
const want = process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'macos' : 'linux';
182201
return flutterDeviceIds().includes(want) ? want : null;
183202
}

‎tests/e2e/mcp-server-smoke-flutter.test.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
* desktop `build()` breakpoint each take ~30 s from a cold example (kernel compile, runner build)
66
* and a few seconds warm; a `flutter test` session ends with `terminated` only, so the session is
77
* `stopped` without an exit code. Self-skips without a Flutter SDK; the desktop cases also skip
8-
* when `flutter devices` lists no desktop target (CI's ubuntu lane runs the widget tests only).
8+
* when `flutter devices` lists no desktop target, on Linux without a display, and anywhere with
9+
* `MCP_SKIP_FLUTTER_DESKTOP=1` (CI's ubuntu lane sets it and runs the widget tests only). The
10+
* tool lists the host desktop whenever the platform feature flag is on, so a Windows box with
11+
* Flutter but no Visual Studio still reaches the runner build and fails there: install the
12+
* toolchain `flutter doctor` names, or set the variable.
913
*/
1014
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
1115
import path from 'path';

0 commit comments

Comments
 (0)