Skip to content

Commit 4e70321

Browse files
Fix integration test failures caused by render overflow errors (#6664)
* Fix integration test failures caused by render overflow errors Any FlutterError reported during a frame (typically "RenderFlex overflowed") fails the on-device testWidgets body, which the exit-code check surfaces as a teardown error on the last test of the module even though all host-side assertions and goldens pass. Test harness: - FletTestApp now captures Flutter test process output and dumps its tail when the process exits non-zero, so CI failures show the reason. - host_test.dart hooks reportTestException to print the exception and stack (flutter test otherwise never prints it) and delays tearDownAll so end-of-run reports are not lost when the process exits. - Screenshot captures are hosted in a hidden-scrollbar scrollable column, so content taller than the test window scrolls instead of overflowing the page; captures and goldens are unaffected. Tests and examples: - test_spinkit: render all spinners inside a scrollable column. - data_table/sortable_and_selectable example: make the page scrollable; the table is 2px taller than the default 800x600 window and drew an overflow on the first frame. - Regenerate stale client/pubspec.lock. CI: temporarily run only the affected tests; restore the full suite list before merging. * Restore full macOS integration test matrix
1 parent 336ed90 commit 4e70321

5 files changed

Lines changed: 139 additions & 20 deletions

File tree

client/pubspec.lock

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,13 @@ packages:
430430
relative: true
431431
source: path
432432
version: "0.1.0"
433+
flet_integration_test:
434+
dependency: "direct dev"
435+
description:
436+
path: "../packages/flet_integration_test"
437+
relative: true
438+
source: path
439+
version: "0.86.0"
433440
flet_lottie:
434441
dependency: "direct main"
435442
description:
@@ -1363,13 +1370,13 @@ packages:
13631370
source: hosted
13641371
version: "2.0.4"
13651372
screen_brightness:
1366-
dependency: transitive
1373+
dependency: "direct overridden"
13671374
description:
13681375
name: screen_brightness
1369-
sha256: e0edf92c08889e8f493cde291e7c687db2b4a1471f2371c074070b75d7c7d79b
1376+
sha256: "5f70754028f169f059fdc61112a19dcbee152f8b293c42c848317854d650cba3"
13701377
url: "https://pub.dev"
13711378
source: hosted
1372-
version: "2.1.11"
1379+
version: "2.1.7"
13731380
screen_brightness_android:
13741381
dependency: transitive
13751382
description:
@@ -1387,13 +1394,13 @@ packages:
13871394
source: hosted
13881395
version: "2.1.4"
13891396
screen_brightness_macos:
1390-
dependency: transitive
1397+
dependency: "direct overridden"
13911398
description:
13921399
name: screen_brightness_macos
1393-
sha256: b0957237b842d846a84363b69f229b339a8f91faced55041e245b2ebff7b0142
1400+
sha256: "278712cf5288db57bd335968cbfb2ec5441028f1ee2fcbdc8d1582d8210a3442"
13941401
url: "https://pub.dev"
13951402
source: hosted
1396-
version: "2.1.4"
1403+
version: "2.1.2"
13971404
screen_brightness_ohos:
13981405
dependency: transitive
13991406
description:

packages/flet_integration_test/lib/src/host_test.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,26 @@ void runFletHostTest({
2525
}) {
2626
var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
2727

28+
// With the integration_test binding `flutter test` prints only
29+
// "Test failed. See exception logs above." without ever printing the
30+
// exception that failed the test (e.g. a render overflow reported during a
31+
// frame). Log it here so the failure reason is visible in the test process
32+
// output.
33+
final prevReportTestException = reportTestException;
34+
reportTestException = (details, testDescription) {
35+
debugPrint("Test exception ($testDescription): "
36+
"${details.exceptionAsString()}\n${details.stack ?? ''}");
37+
prevReportTestException(details, testDescription);
38+
};
39+
debugPrint("Flet host test: exception reporter installed");
40+
41+
tearDownAll(() async {
42+
// Exceptions are reported when the test body completes, moments before
43+
// the app process exits; give the device-log relay time to flush them so
44+
// they are not lost from the `flutter test` output.
45+
await Future.delayed(const Duration(seconds: 2));
46+
});
47+
2848
group('end-to-end test', () {
2949
testWidgets('test app', (tester) async {
3050
var dir = Directory.current.path;

sdk/python/examples/controls/material/data_table/sortable_and_selectable/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33

44
def main(page: ft.Page):
5+
page.scroll = ft.ScrollMode.AUTO
56
inventory_items = [
67
{"id": 1, "name": "Alpha", "qty": 4},
78
{"id": 2, "name": "Bravo", "qty": 9},

sdk/python/packages/flet/integration_tests/extensions/spinkit/test_spinkit.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,19 @@ async def test_all_controls_render(flet_app: ftt.FletTestApp):
138138
]
139139
flet_app.page.clean()
140140
flet_app.page.add(
141-
ft.Row(
142-
wrap=True,
143-
key="row",
144-
controls=[cls(color=ft.Colors.BLUE, size=40) for cls in controls],
141+
# The wrapped row of all spinners is much taller than the test window;
142+
# host it in a scrollable column so the page doesn't overflow (an
143+
# overflow error fails the Flutter test process).
144+
ft.Column(
145+
scroll=ft.ScrollMode.AUTO,
146+
expand=True,
147+
controls=[
148+
ft.Row(
149+
wrap=True,
150+
key="row",
151+
controls=[cls(color=ft.Colors.BLUE, size=40) for cls in controls],
152+
)
153+
],
145154
)
146155
)
147156
await flet_app.tester.pump(duration=ft.Duration(milliseconds=500))

sdk/python/packages/flet/src/flet/testing/flet_test_app.py

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ def __init__(
175175
self.__assets_dir = assets_dir or "assets"
176176
self.__tcp_port = tcp_port
177177
self.__flutter_process: Optional[asyncio.subprocess.Process] = None
178+
self.__flutter_output = bytearray()
179+
self.__flutter_output_task: Optional[asyncio.Task] = None
178180
self.__page = None
179181
self.__tester: Union[Tester, RemoteTester, None] = None
180182

@@ -256,15 +258,14 @@ async def main(page: ft.Page):
256258
print("Started Flet app")
257259

258260
# Stream the Flutter test process output to the console when verbose
259-
# (set by `flet test -v`) or when debug logging is on; otherwise hide it.
260-
stdout = asyncio.subprocess.DEVNULL
261-
stderr = asyncio.subprocess.DEVNULL
262-
if (
261+
# (set by `flet test -v`) or when debug logging is on; otherwise
262+
# capture it into a buffer so it can be dumped if the process fails.
263+
verbose = (
263264
get_bool_env_var("FLET_TEST_VERBOSE")
264265
or logging.getLogger().getEffectiveLevel() == logging.DEBUG
265-
):
266-
stdout = None
267-
stderr = None
266+
)
267+
stdout = None if verbose else asyncio.subprocess.PIPE
268+
stderr = None if verbose else asyncio.subprocess.STDOUT
268269

269270
# The resolved Flutter executable (full path, `flutter.bat` on Windows)
270271
# is passed by `flet test`; fall back to a bare "flutter" on PATH.
@@ -320,6 +321,11 @@ async def main(page: ft.Page):
320321
stderr=stderr,
321322
)
322323

324+
if self.__flutter_process.stdout is not None:
325+
self.__flutter_output_task = asyncio.create_task(
326+
self.__read_flutter_output(self.__flutter_process.stdout)
327+
)
328+
323329
print("Started Flutter test process.")
324330
print("Waiting for the Flutter app to connect...")
325331

@@ -331,11 +337,54 @@ def connected() -> bool:
331337
while not connected():
332338
await asyncio.sleep(0.2)
333339
if self.__flutter_process.returncode is not None:
340+
self.__dump_flutter_output()
334341
raise RuntimeError(
335342
"Flutter process exited early with code "
336343
f"{self.__flutter_process.returncode}"
337344
)
338345

346+
async def __read_flutter_output(self, stream: asyncio.StreamReader):
347+
# Read in chunks, not lines: the Flet client's debug output includes
348+
# very long lines (e.g. screenshot bytes) that overflow StreamReader's
349+
# per-line limit. Overlong lines are cut and only the output tail is
350+
# kept to bound memory.
351+
line = bytearray()
352+
truncated = False
353+
while True:
354+
chunk = await stream.read(65536)
355+
if not chunk:
356+
break
357+
for part in chunk.splitlines(keepends=True):
358+
line.extend(part)
359+
if line.endswith((b"\n", b"\r")):
360+
self.__append_flutter_output_line(line, truncated)
361+
line.clear()
362+
truncated = False
363+
elif len(line) > self.__flutter_output_line_limit:
364+
del line[self.__flutter_output_line_limit :]
365+
truncated = True
366+
if line:
367+
self.__append_flutter_output_line(line, truncated)
368+
369+
def __append_flutter_output_line(self, line: bytearray, truncated: bool):
370+
if len(line) > self.__flutter_output_line_limit:
371+
line = line[: self.__flutter_output_line_limit]
372+
truncated = True
373+
self.__flutter_output.extend(line.rstrip(b"\r\n"))
374+
if truncated:
375+
self.__flutter_output.extend(b" ...<truncated>")
376+
self.__flutter_output.extend(b"\n")
377+
if len(self.__flutter_output) > self.__flutter_output_limit:
378+
del self.__flutter_output[: -self.__flutter_output_limit]
379+
380+
def __dump_flutter_output(self):
381+
if not self.__flutter_output:
382+
return
383+
output = self.__flutter_output.decode(errors="replace")
384+
print("---------- Flutter test process output (tail) ----------")
385+
print(output)
386+
print("---------- End of Flutter test process output ----------")
387+
339388
def __flutter_test_target(self) -> str:
340389
# In device mode the driver (`integration_test/app_test.dart`) is
341390
# generated from the template; validate it exists and is non-empty so a
@@ -383,6 +432,12 @@ async def teardown(self):
383432
print("Force killing Flutter test process...")
384433
self.__flutter_process.kill()
385434

435+
if self.__flutter_output_task:
436+
try:
437+
await asyncio.wait_for(self.__flutter_output_task, timeout=5)
438+
except asyncio.TimeoutError:
439+
self.__flutter_output_task.cancel()
440+
386441
# Stop the RemoteTester socket server (device mode).
387442
if isinstance(self.__tester, RemoteTester):
388443
await self.__tester.stop()
@@ -392,6 +447,7 @@ async def teardown(self):
392447
# `testWidgets` body even though our find/tap assertions passed). Surface
393448
# that as a test failure — otherwise the run is falsely green.
394449
if flutter_returncode is not None and flutter_returncode != 0:
450+
self.__dump_flutter_output()
395451
raise RuntimeError(
396452
f"Flutter integration test process failed with exit code "
397453
f"{flutter_returncode}. See the Flutter test output above."
@@ -420,8 +476,10 @@ async def wrap_page_controls_in_screenshot(
420476
"""
421477
controls = list(self.page.controls)
422478
self.page.controls = [
423-
scr := ft.Screenshot(
424-
ft.Column(controls, margin=margin, intrinsic_width=True)
479+
self.__scrollable_screenshot_host(
480+
scr := ft.Screenshot(
481+
ft.Column(controls, margin=margin, intrinsic_width=True)
482+
)
425483
)
426484
] # type: ignore
427485
self.page.update()
@@ -470,7 +528,11 @@ async def assert_control_screenshot(
470528

471529
# add control and take screenshot
472530
screenshot = ft.Screenshot(control, expand=expand_screenshot)
473-
self.page.add(screenshot)
531+
self.page.add(
532+
screenshot
533+
if expand_screenshot
534+
else self.__scrollable_screenshot_host(screenshot)
535+
)
474536
await self.__pump_and_settle_with_timeout("assert_control_screenshot-add")
475537
for _ in range(0, pump_times):
476538
await self.tester.pump(duration=pump_duration)
@@ -480,6 +542,24 @@ async def assert_control_screenshot(
480542
similarity_threshold=similarity_threshold,
481543
)
482544

545+
def __scrollable_screenshot_host(
546+
self, screenshot: ft.Screenshot
547+
) -> Union[ft.Screenshot, ft.Column]:
548+
# A scrollable page cannot overflow; an expanded host inside it would
549+
# be flex-in-unbounded-height and fail layout instead.
550+
if self.page.scroll is not None:
551+
return screenshot
552+
# Host the screenshot in a scrollable column: content taller than the
553+
# window then scrolls instead of overflowing the page (an overflow
554+
# error fails the Flutter test process). The child sees the same
555+
# unbounded-height constraints as in the page column, so captures are
556+
# unaffected.
557+
return ft.Column(
558+
expand=True,
559+
scroll=ft.ScrollMode.HIDDEN,
560+
controls=[screenshot],
561+
)
562+
483563
async def __pump_and_settle_with_timeout(self, stage: str):
484564
try:
485565
await self.tester.pump_and_settle(timeout=self.__pump_and_settle_timeout)
@@ -849,3 +929,5 @@ def _compare_gifs(
849929
return min(similarities), None
850930

851931
__pump_and_settle_timeout = 10.0
932+
__flutter_output_limit = 262144
933+
__flutter_output_line_limit = 2048

0 commit comments

Comments
 (0)