Re-land "Part 1: Skia Gold Testing" (#36103)
diff --git a/.cirrus.yml b/.cirrus.yml index debe342..3d2134f 100644 --- a/.cirrus.yml +++ b/.cirrus.yml
@@ -63,6 +63,9 @@ GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] SHARD: tests SUBSHARD: widgets + GOLDCTL: "$CIRRUS_WORKING_DIR/depot_tools/goldctl" + GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095] + goldctl_script: ./dev/bots/download_goldctl.sh test_script: - dart --enable-asserts ./dev/bots/test.dart container: @@ -73,6 +76,9 @@ GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] SHARD: tests SUBSHARD: framework_other + GOLDCTL: "$CIRRUS_WORKING_DIR/depot_tools/goldctl" + GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095] + goldctl_script: ./dev/bots/download_goldctl.sh test_script: - dart --enable-asserts ./dev/bots/test.dart container: @@ -310,6 +316,9 @@ GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] SHARD: tests SUBSHARD: widgets + GOLDCTL: "C:\\Windows\\Temp\\depot_tools\\goldctl.exe" + GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095] + goldctl_script: powershell dev\bots\download_goldctl.ps1 test_all_script: - dart --enable-asserts dev\bots\test.dart - name: tests_framework_other-windows @@ -317,6 +326,9 @@ GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] SHARD: tests SUBSHARD: framework_other + GOLDCTL: "C:\\Windows\\Temp\\depot_tools\\goldctl.exe" + GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095] + goldctl_script: powershell dev\bots\download_goldctl.ps1 test_all_script: - dart --enable-asserts dev\bots\test.dart - name: tests_extras-windows @@ -441,7 +453,7 @@ task: use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' osx_instance: - image: mojave-xcode-10.1 + image: mojave-xcode-10.2 depends_on: - analyze env: @@ -477,17 +489,27 @@ GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] SHARD: tests SUBSHARD: widgets + GOLDCTL: "$CIRRUS_WORKING_DIR/depot_tools/goldctl" + GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095] + goldctl_script: ./dev/bots/download_goldctl.sh test_all_script: - ulimit -S -n 2048 # https://github.com/flutter/flutter/issues/2976 - dart --enable-asserts dev/bots/test.dart + on_failure: + print_failure_time_script: date - name: tests_framework_other-macos env: GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] SHARD: tests SUBSHARD: framework_other + GOLDCTL: "$CIRRUS_WORKING_DIR/depot_tools/goldctl" + GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095] + goldctl_script: ./dev/bots/download_goldctl.sh test_all_script: - ulimit -S -n 2048 # https://github.com/flutter/flutter/issues/2976 - dart --enable-asserts dev/bots/test.dart + on_failure: + print_failure_time_script: date - name: tests_extras-macos env: GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb]
diff --git a/dev/bots/download_goldctl.ps1 b/dev/bots/download_goldctl.ps1 new file mode 100644 index 0000000..f83cd8c --- /dev/null +++ b/dev/bots/download_goldctl.ps1
@@ -0,0 +1,14 @@ +$url= "https://storage.googleapis.com/chrome-infra/depot_tools.zip" +$zipPath = "C:\Windows\Temp\depot_tools.zip" +$path = "C:\Windows\Temp\depot_tools" +$gclient = "C:\Windows\Temp\depot_tools\gclient.bat" +$cipd = "C:\Windows\Temp\depot_tools\cipd.bat" +$ensureFile = "C:\Windows\Temp\depot_tools\ensure.txt" +$text = "# Ensure File`n`$ServiceURL https://chrome-infra-packages.appspot.com`n`n# Skia Gold Client goldctl`nskia/tools/goldctl/`${platform} latest" + +(New-Object System.Net.WebClient).DownloadFile($url, $zipPath) +Expand-Archive -LiteralPath $zipPath -DestinationPath $path +cd $path +cmd.exe /C "$gclient" +$text | Out-File -filePath $ensureFile -encoding ascii +cmd.exe /C "$cipd ensure -ensure-file $ensureFile -root $path"
diff --git a/dev/bots/download_goldctl.sh b/dev/bots/download_goldctl.sh new file mode 100755 index 0000000..7fcaa1f --- /dev/null +++ b/dev/bots/download_goldctl.sh
@@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +git clone --depth 1 https://chromium.googlesource.com/chromium/tools/depot_tools.git ./depot_tools +cd depot_tools +echo -e '# Ensure File\n$ServiceURL https://chrome-infra-packages.appspot.com\n\n# Skia Gold Client goldctl\nskia/tools/goldctl/${platform} latest' > ensure.txt +./cipd ensure -ensure-file ./ensure.txt -root .
diff --git a/packages/flutter/test/cupertino/date_picker_test.dart b/packages/flutter/test/cupertino/date_picker_test.dart index ae90212..0b8908f 100644 --- a/packages/flutter/test/cupertino/date_picker_test.dart +++ b/packages/flutter/test/cupertino/date_picker_test.dart
@@ -847,7 +847,6 @@ 'date_picker_test.datetime.initial.png', version: 1, ), - skip: !isLinux ); // Slightly drag the hour component to make the current hour off-center. @@ -860,7 +859,6 @@ 'date_picker_test.datetime.drag.png', version: 1, ), - skip: !isLinux ); }); });
diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart index 96afba3..56afa4a 100644 --- a/packages/flutter/test/cupertino/nav_bar_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_test.dart
@@ -805,9 +805,6 @@ ), ); }, - // TODO(xster): remove once https://github.com/flutter/flutter/issues/17483 - // is fixed. - skip: !isLinux, ); testWidgets( @@ -842,10 +839,7 @@ ), ); }, - // TODO(xster): remove once https://github.com/flutter/flutter/issues/17483 - // is fixed. - skip: !isLinux, - ); + ); testWidgets('NavBar draws a light system bar for a dark background', (WidgetTester tester) async {
diff --git a/packages/flutter/test/cupertino/segmented_control_test.dart b/packages/flutter/test/cupertino/segmented_control_test.dart index 4cacef6..71556c8 100644 --- a/packages/flutter/test/cupertino/segmented_control_test.dart +++ b/packages/flutter/test/cupertino/segmented_control_test.dart
@@ -1417,7 +1417,7 @@ version: 0, ), ); - }, skip: !isLinux); + }); testWidgets('Golden Test Pressed State', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; @@ -1458,5 +1458,5 @@ version: 0, ), ); - }, skip: !isLinux); + }); }
diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index a4ebbd1..dcdc751 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart
@@ -481,7 +481,7 @@ version: 2, ), ); - }, skip: !isLinux); + }); testWidgets('Cupertino cursor iOS golden', (WidgetTester tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; @@ -514,7 +514,7 @@ version: 2, ), ); - }, skip: !isLinux); + }); testWidgets( 'can control text content via controller', @@ -2901,7 +2901,6 @@ 'text_field_test.disabled.png', version: 0, ), - skip: !isLinux, ); });
diff --git a/packages/flutter/test/material/bottom_app_bar_test.dart b/packages/flutter/test/material/bottom_app_bar_test.dart index 4a07398..7f1b3dc 100644 --- a/packages/flutter/test/material/bottom_app_bar_test.dart +++ b/packages/flutter/test/material/bottom_app_bar_test.dart
@@ -75,7 +75,6 @@ 'bottom_app_bar.custom_shape.1.png', version: null, ), - skip: !isLinux, ); await pump(FloatingActionButtonLocation.centerDocked); await tester.pumpAndSettle(); @@ -85,7 +84,6 @@ 'bottom_app_bar.custom_shape.2.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser);
diff --git a/packages/flutter/test/material/bottom_app_bar_theme_test.dart b/packages/flutter/test/material/bottom_app_bar_theme_test.dart index 4bbb597..3b4cd47 100644 --- a/packages/flutter/test/material/bottom_app_bar_theme_test.dart +++ b/packages/flutter/test/material/bottom_app_bar_theme_test.dart
@@ -84,7 +84,6 @@ 'bottom_app_bar_theme.custom_shape.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser);
diff --git a/packages/flutter/test/material/bottom_navigation_bar_test.dart b/packages/flutter/test/material/bottom_navigation_bar_test.dart index e8b277c..d8bc3a7 100644 --- a/packages/flutter/test/material/bottom_navigation_bar_test.dart +++ b/packages/flutter/test/material/bottom_navigation_bar_test.dart
@@ -1438,7 +1438,6 @@ 'bottom_navigation_bar.shifting_transition.$pump.png', version: 2, ), - skip: !isLinux, ); } }, skip: isBrowser);
diff --git a/packages/flutter/test/material/card_theme_test.dart b/packages/flutter/test/material/card_theme_test.dart index 8dedd6c..abdf8cc 100644 --- a/packages/flutter/test/material/card_theme_test.dart +++ b/packages/flutter/test/material/card_theme_test.dart
@@ -141,7 +141,6 @@ 'card_theme.custom_shape.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); }
diff --git a/packages/flutter/test/material/dialog_theme_test.dart b/packages/flutter/test/material/dialog_theme_test.dart index 64378ab..133ecc1 100644 --- a/packages/flutter/test/material/dialog_theme_test.dart +++ b/packages/flutter/test/material/dialog_theme_test.dart
@@ -134,7 +134,6 @@ 'dialog_theme.dialog_with_custom_border.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser);
diff --git a/packages/flutter/test/material/dropdown_test.dart b/packages/flutter/test/material/dropdown_test.dart index b16d9bc..03c480c 100644 --- a/packages/flutter/test/material/dropdown_test.dart +++ b/packages/flutter/test/material/dropdown_test.dart
@@ -144,7 +144,6 @@ 'dropdown_test.default.png', version: 0, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -160,7 +159,6 @@ 'dropdown_test.expanded.png', version: 0, ), - skip: !isLinux, ); }, skip: isBrowser);
diff --git a/packages/flutter/test/material/floating_action_button_test.dart b/packages/flutter/test/material/floating_action_button_test.dart index 8b89ba8..5be8270 100644 --- a/packages/flutter/test/material/floating_action_button_test.dart +++ b/packages/flutter/test/material/floating_action_button_test.dart
@@ -740,7 +740,6 @@ 'floating_action_button_test.clip.png', version: 2, ), - skip: !isLinux, ); });
diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index ad5739a..f18d1dd 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart
@@ -2865,7 +2865,6 @@ 'input_decorator.outline_icon_label.ltr.png', version: null, ), - skip: !isLinux, ); await tester.pumpWidget(buildFrame(TextDirection.rtl)); @@ -2875,10 +2874,8 @@ 'input_decorator.outline_icon_label.rtl.png', version: null, ), - skip: !isLinux, ); }, - skip: !isLinux, ); testWidgets('InputDecorator draws and animates hoverColor', (WidgetTester tester) async {
diff --git a/packages/flutter/test/material/material_test.dart b/packages/flutter/test/material/material_test.dart index 4a7cb82..d3afca7 100644 --- a/packages/flutter/test/material/material_test.dart +++ b/packages/flutter/test/material/material_test.dart
@@ -698,7 +698,6 @@ 'material.border_paint_above.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -742,7 +741,6 @@ 'material.border_paint_below.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); });
diff --git a/packages/flutter/test/material/radio_test.dart b/packages/flutter/test/material/radio_test.dart index 29d8748..bf817a5 100644 --- a/packages/flutter/test/material/radio_test.dart +++ b/packages/flutter/test/material/radio_test.dart
@@ -280,7 +280,6 @@ 'radio.ink_ripple.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); }
diff --git a/packages/flutter/test/material/tab_bar_theme_test.dart b/packages/flutter/test/material/tab_bar_theme_test.dart index f17d4e0..bb97c5d 100644 --- a/packages/flutter/test/material/tab_bar_theme_test.dart +++ b/packages/flutter/test/material/tab_bar_theme_test.dart
@@ -271,7 +271,6 @@ 'tab_bar_theme.tab_indicator_size_tab.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -286,7 +285,6 @@ 'tab_bar_theme.tab_indicator_size_label.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -306,7 +304,6 @@ 'tab_bar_theme.custom_tab_indicator.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -326,7 +323,6 @@ 'tab_bar_theme.beveled_rect_indicator.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); }
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index d0f5de1..e75ed5f 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart
@@ -416,7 +416,7 @@ version: 0, ), ); - }, skip: !isLinux); + }); testWidgets('Material cursor iOS golden', (WidgetTester tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; @@ -448,7 +448,7 @@ version: 0, ), ); - }, skip: !isLinux); + }); testWidgets('text field selection toolbar renders correctly inside opacity', (WidgetTester tester) async { await tester.pumpWidget( @@ -501,7 +501,6 @@ 'text_field_opacity_test.0.png', version: 2, ), - skip: !isLinux, ); }, skip: isBrowser);
diff --git a/packages/flutter/test/painting/continous_rectangle_border_test.dart b/packages/flutter/test/painting/continous_rectangle_border_test.dart index 70e4526..f644fb0 100644 --- a/packages/flutter/test/painting/continous_rectangle_border_test.dart +++ b/packages/flutter/test/painting/continous_rectangle_border_test.dart
@@ -75,7 +75,6 @@ 'continuous_rectangle_border.golden_test_even_radii.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -100,7 +99,6 @@ 'continuous_rectangle_border.golden_test_varying_radii.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -122,7 +120,6 @@ 'continuous_rectangle_border.golden_test_large_radii.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser);
diff --git a/packages/flutter/test/rendering/localized_fonts_test.dart b/packages/flutter/test/rendering/localized_fonts_test.dart index 025bb56..e269699 100644 --- a/packages/flutter/test/rendering/localized_fonts_test.dart +++ b/packages/flutter/test/rendering/localized_fonts_test.dart
@@ -55,7 +55,6 @@ ), ); }, - skip: !isLinux, ); testWidgets( @@ -110,7 +109,6 @@ ), ); }, - skip: !isLinux, ); testWidgets( @@ -157,7 +155,6 @@ ), ); }, - skip: !isLinux, ); }
diff --git a/packages/flutter/test/widgets/backdrop_filter_test.dart b/packages/flutter/test/widgets/backdrop_filter_test.dart index c728f50..db339be 100644 --- a/packages/flutter/test/widgets/backdrop_filter_test.dart +++ b/packages/flutter/test/widgets/backdrop_filter_test.dart
@@ -47,7 +47,6 @@ 'backdrop_filter_test.cull_rect.png', version: 1, ), - skip: !isLinux, ); }, skip: isBrowser); }
diff --git a/packages/flutter/test/widgets/color_filter_test.dart b/packages/flutter/test/widgets/color_filter_test.dart index 2f09cd1..469c508 100644 --- a/packages/flutter/test/widgets/color_filter_test.dart +++ b/packages/flutter/test/widgets/color_filter_test.dart
@@ -21,7 +21,6 @@ 'color_filter_red.png', version: 1, ), - skip: !isLinux ); }); @@ -63,7 +62,6 @@ 'color_filter_sepia.png', version: 1, ), - skip: !isLinux ); }); } \ No newline at end of file
diff --git a/packages/flutter/test/widgets/editable_text_cursor_test.dart b/packages/flutter/test/widgets/editable_text_cursor_test.dart index 23292da..91dec60 100644 --- a/packages/flutter/test/widgets/editable_text_cursor_test.dart +++ b/packages/flutter/test/widgets/editable_text_cursor_test.dart
@@ -95,7 +95,7 @@ version: 3, ), ); - }, skip: !isLinux); + }); testWidgets('cursor layout has correct radius', (WidgetTester tester) async { final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); @@ -149,7 +149,7 @@ version: 3, ), ); - }, skip: !isLinux); + }); testWidgets('Cursor animates on iOS', (WidgetTester tester) async { final Widget widget = MaterialApp( @@ -803,6 +803,5 @@ ), ); debugDefaultTargetPlatformOverride = null; - }, skip: !isLinux); - + }); }
diff --git a/packages/flutter/test/widgets/invert_colors_test.dart b/packages/flutter/test/widgets/invert_colors_test.dart index bedb76c..8489f6e 100644 --- a/packages/flutter/test/widgets/invert_colors_test.dart +++ b/packages/flutter/test/widgets/invert_colors_test.dart
@@ -24,7 +24,6 @@ 'invert_colors_test.0.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -46,7 +45,6 @@ 'invert_colors_test.1.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); }
diff --git a/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart b/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart index eacb3e5..b52e538 100644 --- a/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart +++ b/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart
@@ -539,7 +539,6 @@ 'list_wheel_scroll_view.center_child.magnified.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -597,7 +596,6 @@ 'list_wheel_scroll_view.curved_wheel.left.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser);
diff --git a/packages/flutter/test/widgets/opacity_test.dart b/packages/flutter/test/widgets/opacity_test.dart index 0446459..1d173b3 100644 --- a/packages/flutter/test/widgets/opacity_test.dart +++ b/packages/flutter/test/widgets/opacity_test.dart
@@ -181,7 +181,6 @@ 'opacity_test.offset.png', version: 1, ), - skip: !isLinux, ); }, skip: isBrowser);
diff --git a/packages/flutter/test/widgets/physical_model_test.dart b/packages/flutter/test/widgets/physical_model_test.dart index c425fb4..5e043f8 100644 --- a/packages/flutter/test/widgets/physical_model_test.dart +++ b/packages/flutter/test/widgets/physical_model_test.dart
@@ -114,7 +114,6 @@ 'physical_model_overflow.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser);
diff --git a/packages/flutter/test/widgets/shadow_test.dart b/packages/flutter/test/widgets/shadow_test.dart index bf5d6e2..f573974 100644 --- a/packages/flutter/test/widgets/shadow_test.dart +++ b/packages/flutter/test/widgets/shadow_test.dart
@@ -37,8 +37,7 @@ 'shadow.BoxDecoration.enabled.png', version: null, ), - skip: !isLinux - ); // shadows render differently on different platforms + ); debugDisableShadows = true; }, skip: isBrowser); @@ -70,7 +69,7 @@ ); } debugDisableShadows = true; - }, skip: !isLinux); // shadows render differently on different platforms + }); testWidgets('Shadows with PhysicalLayer', (WidgetTester tester) async { await tester.pumpWidget( @@ -107,8 +106,7 @@ 'shadow.PhysicalModel.enabled.png', version: null, ), - skip: !isLinux, - ); // shadows render differently on different platforms + ); debugDisableShadows = true; }, skip: isBrowser); @@ -144,5 +142,5 @@ ); } debugDisableShadows = true; - }, skip: !isLinux); // shadows render differently on different platforms + }); }
diff --git a/packages/flutter/test/widgets/text_golden_test.dart b/packages/flutter/test/widgets/text_golden_test.dart index ce798a0..4b0d29a 100644 --- a/packages/flutter/test/widgets/text_golden_test.dart +++ b/packages/flutter/test/widgets/text_golden_test.dart
@@ -62,7 +62,7 @@ version: null, ), ); - }, skip: !isLinux); + }); testWidgets('Text Foreground', (WidgetTester tester) async { @@ -147,7 +147,7 @@ version: null, ), ); - }, skip: !isLinux); + }); // TODO(garyq): This test requires an update when the background // drawing from the beginning of the line bug is fixed. The current @@ -200,7 +200,7 @@ version: null, ), ); - }, skip: !isLinux); + }); testWidgets('Text Fade', (WidgetTester tester) async { await tester.pumpWidget( @@ -239,7 +239,7 @@ version: 1, ), ); - }, skip: !isLinux); + }); testWidgets('Default Strut text', (WidgetTester tester) async { await tester.pumpWidget( @@ -267,8 +267,7 @@ version: null, ), ); - }, skip: true); // Should only be on linux (skip: !isLinux). - // Disabled for now until font inconsistency is resolved. + }); testWidgets('Strut text 1', (WidgetTester tester) async { await tester.pumpWidget( @@ -298,8 +297,7 @@ version: 1, ), ); - }, skip: true); // Should only be on linux (skip: !isLinux). - // Disabled for now until font inconsistency is resolved. + }); testWidgets('Strut text 2', (WidgetTester tester) async { await tester.pumpWidget( @@ -330,8 +328,7 @@ version: 1, ), ); - }, skip: true); // Should only be on linux (skip: !isLinux). - // Disabled for now until font inconsistency is resolved. + }); testWidgets('Strut text rich', (WidgetTester tester) async { await tester.pumpWidget( @@ -385,8 +382,7 @@ version: 1, ), ); - }, skip: true); // Should only be on linux (skip: !isLinux). - // Disabled for now until font inconsistency is resolved. + }); testWidgets('Strut text font fallback', (WidgetTester tester) async { // Font Fallback @@ -424,8 +420,7 @@ version: 1, ), ); - }, skip: true); // Should only be on linux (skip: !isLinux). - // Disabled for now until font inconsistency is resolved. + }); testWidgets('Strut text rich forceStrutHeight', (WidgetTester tester) async { await tester.pumpWidget( @@ -479,8 +474,7 @@ version: 1, ), ); - }, skip: true); // Should only be on linux (skip: !isLinux). - // Disabled for now until font inconsistency is resolved. + }); testWidgets('Decoration thickness', (WidgetTester tester) async { final TextDecoration allDecorations = TextDecoration.combine( @@ -521,7 +515,7 @@ version: 0, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Decoration thickness', (WidgetTester tester) async { final TextDecoration allDecorations = TextDecoration.combine( @@ -563,7 +557,7 @@ version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget', (WidgetTester tester) async { await tester.pumpWidget( @@ -660,7 +654,7 @@ version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget textfield', (WidgetTester tester) async { await tester.pumpWidget( @@ -708,7 +702,7 @@ version: 2, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); // This tests if multiple Text.rich widgets are able to inline nest within each other. testWidgets('Text Inline widget nesting', (WidgetTester tester) async { @@ -840,7 +834,7 @@ version: 2, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget baseline', (WidgetTester tester) async { await tester.pumpWidget( @@ -950,7 +944,7 @@ version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget aboveBaseline', (WidgetTester tester) async { await tester.pumpWidget( @@ -1060,7 +1054,7 @@ version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget belowBaseline', (WidgetTester tester) async { await tester.pumpWidget( @@ -1170,7 +1164,7 @@ version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget top', (WidgetTester tester) async { await tester.pumpWidget( @@ -1280,7 +1274,7 @@ version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget middle', (WidgetTester tester) async { await tester.pumpWidget( @@ -1390,5 +1384,5 @@ version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); }
diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index dc77bd4..70e64a4 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart
@@ -2028,7 +2028,6 @@ 'inspector.repaint_boundary_margin.png', version: null, ), - skip: !isLinux, ); // Regression test for how rendering with a pixel scale other than 1.0 @@ -2042,7 +2041,6 @@ 'inspector.repaint_boundary_margin_small.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2054,7 +2052,6 @@ 'inspector.repaint_boundary_margin_large.png', version: null, ), - skip: !isLinux, ); final Layer layerParent = layer.parent; @@ -2073,7 +2070,6 @@ 'inspector.repaint_boundary.png', version: null, ), - skip: !isLinux, ); // Verify that taking a screenshot didn't change the layers associated with @@ -2094,7 +2090,6 @@ 'inspector.repaint_boundary_margin.png', version: null, ), - skip: !isLinux, ); // Verify that taking a screenshot didn't change the layers associated with @@ -2118,7 +2113,6 @@ 'inspector.repaint_boundary_debugPaint.png', version: null, ), - skip: !isLinux, ); // Verify that taking a screenshot with debug paint on did not change // the number of children the layer has. @@ -2132,7 +2126,6 @@ 'inspector.repaint_boundary.png', version: null, ), - skip: !isLinux, ); expect(renderObject.debugLayer, equals(layer)); @@ -2149,7 +2142,6 @@ 'inspector.container.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2163,7 +2155,6 @@ 'inspector.container_debugPaint.png', version: null, ), - skip: !isLinux, ); { @@ -2187,7 +2178,6 @@ 'inspector.container_debugPaint.png', version: null, ), - skip: !isLinux, ); expect(container.debugNeedsLayout, isFalse); } @@ -2203,7 +2193,6 @@ 'inspector.container_small.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2217,7 +2206,6 @@ 'inspector.container_large.png', version: null, ), - skip: !isLinux, ); // This screenshot will show the clip rect debug paint but no other @@ -2233,7 +2221,6 @@ 'inspector.clipRect_debugPaint.png', version: null, ), - skip: !isLinux, ); final Element clipRect = find.byType(ClipRRect).evaluate().single; @@ -2253,7 +2240,6 @@ 'inspector.clipRect_debugPaint_margin.png', version: null, ), - skip: !isLinux, ); // Verify we get the same image if we go through the service extension @@ -2296,7 +2282,6 @@ 'inspector.padding_debugPaint.png', version: null, ), - skip: !isLinux, ); // The bounds for this box crop its rendered content. @@ -2311,7 +2296,6 @@ 'inspector.sizedBox_debugPaint.png', version: 1, ), - skip: !isLinux, ); // Verify that setting a margin includes the previously cropped content. @@ -2327,7 +2311,6 @@ 'inspector.sizedBox_debugPaint_margin.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -2467,7 +2450,6 @@ 'inspector.composited_transform.only_offsets.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2480,7 +2462,6 @@ 'inspector.composited_transform.only_offsets_follower.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2489,7 +2470,6 @@ 'inspector.composited_transform.only_offsets_small.png', version: 1, ), - skip: !isLinux, ); await expectLater( @@ -2502,7 +2482,6 @@ 'inspector.composited_transform.only_offsets_target.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -2578,7 +2557,6 @@ 'inspector.composited_transform.with_rotations.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2591,7 +2569,6 @@ 'inspector.composited_transform.with_rotations_small.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2604,7 +2581,6 @@ 'inspector.composited_transform.with_rotations_target.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2617,7 +2593,6 @@ 'inspector.composited_transform.with_rotations_follower.png', version: null, ), - skip: !isLinux, ); // Make sure taking screenshots hasn't modified the positions of the
diff --git a/packages/flutter_goldens/lib/flutter_goldens.dart b/packages/flutter_goldens/lib/flutter_goldens.dart index 4f28df3..7a5b96d 100644 --- a/packages/flutter_goldens/lib/flutter_goldens.dart +++ b/packages/flutter_goldens/lib/flutter_goldens.dart
@@ -9,19 +9,88 @@ import 'package:file/local.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:meta/meta.dart'; +import 'package:platform/platform.dart'; import 'package:flutter_goldens_client/client.dart'; +import 'package:flutter_goldens_client/skia_client.dart'; + export 'package:flutter_goldens_client/client.dart'; +export 'package:flutter_goldens_client/skia_client.dart'; /// Main method that can be used in a `flutter_test_config.dart` file to set /// [goldenFileComparator] to an instance of [FlutterGoldenFileComparator] that -/// works for the current test. +/// works for the current test. _Which_ FlutterGoldenFileComparator is +/// instantiated is based on the current testing environment. Future<void> main(FutureOr<void> testMain()) async { - goldenFileComparator = await FlutterGoldenFileComparator.fromDefaultComparator(); + const Platform platform = LocalPlatform(); + if (FlutterSkiaGoldFileComparator.isAvailableOnPlatform(platform)) { + goldenFileComparator = await FlutterSkiaGoldFileComparator.fromDefaultComparator(); + } else if (FlutterGoldensRepositoryFileComparator.isAvailableOnPlatform(platform)) { + goldenFileComparator = await FlutterGoldensRepositoryFileComparator.fromDefaultComparator(); + } else { + goldenFileComparator = FlutterSkippingGoldenFileComparator.fromDefaultComparator(); + } await testMain(); } -/// A golden file comparator specific to the `flutter/flutter` repository. +/// Abstract base class golden file comparator specific to the `flutter/flutter` +/// repository. +abstract class FlutterGoldenFileComparator extends GoldenFileComparator { + /// Creates a [FlutterGoldenFileComparator] that will resolve golden file + /// URIs relative to the specified [basedir]. + /// + /// The [fs] and [platform] parameters useful in tests, where the default file + /// system and platform can be replaced by mock instances. + @visibleForTesting + FlutterGoldenFileComparator( + this.basedir, { + this.fs = const LocalFileSystem(), + this.platform = const LocalPlatform(), + }) : assert(basedir != null), + assert(fs != null), + assert(platform != null); + + /// The directory to which golden file URIs will be resolved in [compare] and + /// [update]. + final Uri basedir; + + /// The file system used to perform file access. + @visibleForTesting + final FileSystem fs; + + /// A wrapper for the [dart:io.Platform] API. + @visibleForTesting + final Platform platform; + + @override + Future<void> update(Uri golden, Uint8List imageBytes) async { + final File goldenFile = getGoldenFile(golden); + await goldenFile.parent.create(recursive: true); + await goldenFile.writeAsBytes(imageBytes, flush: true); + } + + /// Calculate the appropriate basedir for the current test context. + @protected + @visibleForTesting + static Directory getBaseDirectory(GoldensClient goldens, LocalFileComparator defaultComparator) { + final FileSystem fs = goldens.fs; + final Directory testDirectory = fs.directory(defaultComparator.basedir); + final String testDirectoryRelativePath = fs.path.relative(testDirectory.path, from: goldens.flutterRoot.path); + return goldens.comparisonRoot.childDirectory(testDirectoryRelativePath); + } + + /// Returns the golden [File] identified by the given [Uri]. + @protected + File getGoldenFile(Uri uri) { + assert(basedir.scheme == 'file'); + final File goldenFile = fs.directory(basedir).childFile(fs.file(uri).path); + assert(goldenFile.uri.scheme == 'file'); + return goldenFile; + } +} + +/// A [FlutterGoldenFileComparator] for testing golden images against the +/// `flutter/goldens` repository. /// /// Within the https://github.com/flutter/flutter repository, it's important /// not to check-in binaries in order to keep the size of the repository to a @@ -29,59 +98,61 @@ /// files from a sibling repository, `flutter/goldens`. /// /// This comparator will locally clone the `flutter/goldens` repository into -/// the `$FLUTTER_ROOT/bin/cache/pkg/goldens` folder, then perform the comparison against -/// the files therein. -class FlutterGoldenFileComparator implements GoldenFileComparator { - /// Creates a [FlutterGoldenFileComparator] that will resolve golden file - /// URIs relative to the specified [basedir]. +/// the `$FLUTTER_ROOT/bin/cache/pkg/goldens` folder using the +/// [GoldensRepositoryClient], then perform the comparison against the files +/// therein. +/// +/// See also: +/// +/// * [GoldenFileComparator], the abstract class that +/// [FlutterGoldenFileComparator] implements. +/// * [FlutterSkiaGoldFileComparator], another [FlutterGoldenFileComparator] +/// that tests golden images through Skia Gold. +class FlutterGoldensRepositoryFileComparator extends FlutterGoldenFileComparator { + /// Creates a [FlutterGoldensRepositoryFileComparator] that will test golden + /// file images against the `flutter/goldens` repository. /// - /// The [fs] parameter exists for testing purposes only. - @visibleForTesting - FlutterGoldenFileComparator( - this.basedir, { - this.fs = const LocalFileSystem(), - }); + /// The [fs] and [platform] parameters useful in tests, where the default file + /// system and platform can be replaced by mock instances. + FlutterGoldensRepositoryFileComparator( + Uri basedir, { + FileSystem fs = const LocalFileSystem(), + Platform platform = const LocalPlatform(), + }) : super( + basedir, + fs: fs, + platform: platform, + ); - /// The directory to which golden file URIs will be resolved in [compare] and [update]. - final Uri basedir; - - /// The file system used to perform file access. - @visibleForTesting - final FileSystem fs; - - /// Creates a new [FlutterGoldenFileComparator] that mirrors the relative - /// path resolution of the default [goldenFileComparator]. + /// Creates a new [FlutterGoldensRespositoryFileComparator] that mirrors the + /// relative path resolution of the default [goldenFileComparator]. /// /// By the time the future completes, the clone of the `flutter/goldens` - /// repository is guaranteed to be ready use. + /// repository is guaranteed to be ready to use. /// /// The [goldens] and [defaultComparator] parameters are visible for testing /// purposes only. - static Future<FlutterGoldenFileComparator> fromDefaultComparator({ - GoldensClient goldens, + static Future<FlutterGoldensRepositoryFileComparator> fromDefaultComparator({ + GoldensRepositoryClient goldens, LocalFileComparator defaultComparator, }) async { defaultComparator ??= goldenFileComparator; // Prepare the goldens repo. - goldens ??= GoldensClient(); + goldens ??= GoldensRepositoryClient(); await goldens.prepare(); - // Calculate the appropriate basedir for the current test context. - final FileSystem fs = goldens.fs; - final Directory testDirectory = fs.directory(defaultComparator.basedir); - final String testDirectoryRelativePath = fs.path.relative(testDirectory.path, from: goldens.flutterRoot.path); - return FlutterGoldenFileComparator(goldens.repositoryRoot.childDirectory(testDirectoryRelativePath).uri); + final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(goldens, defaultComparator); + return FlutterGoldensRepositoryFileComparator(baseDirectory.uri); } @override Future<bool> compare(Uint8List imageBytes, Uri golden) async { - final File goldenFile = _getGoldenFile(golden); + final File goldenFile = getGoldenFile(golden); if (!goldenFile.existsSync()) { throw TestFailure('Could not be compared against non-existent file: "$golden"'); } final List<int> goldenBytes = await goldenFile.readAsBytes(); - // TODO(tvolkert): Improve the intelligence of this comparison. if (goldenBytes.length != imageBytes.length) { return false; } @@ -93,14 +164,134 @@ return true; } - @override - Future<void> update(Uri golden, Uint8List imageBytes) async { - final File goldenFile = _getGoldenFile(golden); - await goldenFile.parent.create(recursive: true); - await goldenFile.writeAsBytes(imageBytes, flush: true); + /// Decides based on the current platform whether goldens tests should be + /// performed against the flutter/goldens repository. + static bool isAvailableOnPlatform(Platform platform) => platform.isLinux; +} + +/// A [FlutterGoldenFileComparator] for testing golden images with Skia Gold. +/// +/// For testing across all platforms, the [SkiaGoldClient] is used to upload +/// images for framework-related golden tests and process results. Currently +/// these tests are designed to be run post-submit on Cirrus CI, informed by the +/// environment. +/// +/// See also: +/// +/// * [GoldenFileComparator], the abstract class that +/// [FlutterGoldenFileComparator] implements. +/// * [FlutterGoldensRepositoryFileComparator], another +/// [FlutterGoldenFileComparator] that tests golden images using the +/// flutter/goldens repository. +class FlutterSkiaGoldFileComparator extends FlutterGoldenFileComparator { + /// Creates a [FlutterSkiaGoldFileComparator] that will test golden file + /// images against Skia Gold. + /// + /// The [fs] and [platform] parameters useful in tests, where the default file + /// system and platform can be replaced by mock instances. + FlutterSkiaGoldFileComparator( + final Uri basedir, + this.skiaClient, { + FileSystem fs = const LocalFileSystem(), + Platform platform = const LocalPlatform(), + }) : super( + basedir, + fs: fs, + platform: platform, + ); + + final SkiaGoldClient skiaClient; + + /// Creates a new [FlutterSkiaGoldFileComparator] that mirrors the relative + /// path resolution of the default [goldenFileComparator]. + /// + /// The [goldens] and [defaultComparator] parameters are visible for testing + /// purposes only. + static Future<FlutterSkiaGoldFileComparator> fromDefaultComparator({ + SkiaGoldClient goldens, + LocalFileComparator defaultComparator, + }) async { + defaultComparator ??= goldenFileComparator; + goldens ??= SkiaGoldClient(); + + final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(goldens, defaultComparator); + if (!baseDirectory.existsSync()) + baseDirectory.createSync(recursive: true); + await goldens.auth(baseDirectory); + await goldens.imgtestInit(); + return FlutterSkiaGoldFileComparator(baseDirectory.uri, goldens); } - File _getGoldenFile(Uri uri) { - return fs.directory(basedir).childFile(fs.file(uri).path); + @override + Future<bool> compare(Uint8List imageBytes, Uri golden) async { + golden = _addPrefix(golden); + await update(golden, imageBytes); + + final File goldenFile = getGoldenFile(golden); + if (!goldenFile.existsSync()) { + throw TestFailure('Could not be compared against non-existent file: "$golden"'); + } + return await skiaClient.imgtestAdd(golden.path, goldenFile); } + + @override + Uri getTestUri(Uri key, int version) => key; + + /// Decides based on the current environment whether goldens tests should be + /// performed against Skia Gold. + static bool isAvailableOnPlatform(Platform platform) { + final String cirrusCI = platform.environment['CIRRUS_CI'] ?? ''; + final String cirrusPR = platform.environment['CIRRUS_PR'] ?? ''; + final String cirrusBranch = platform.environment['CIRRUS_BRANCH'] ?? ''; + final String goldServiceAccount = platform.environment['GOLD_SERVICE_ACCOUNT'] ?? ''; + return cirrusCI.isNotEmpty + && cirrusPR.isEmpty + && cirrusBranch == 'master' + && goldServiceAccount.isNotEmpty; + } + + /// Prepends the golden Uri with the library name that encloses the current + /// test. + Uri _addPrefix(Uri golden) { + final String prefix = basedir.pathSegments[basedir.pathSegments.length - 2]; + return Uri.parse(prefix + '.' + golden.toString()); + } +} + +/// A [FlutterGoldenFileComparator] for skipping golden image tests when Skia +/// Gold is unavailable or the current platform that is executing tests is not +/// Linux. +/// +/// See also: +/// +/// * [FlutterGoldensRepositoryFileComparator], another +/// [FlutterGoldenFileComparator] that tests golden images using the +/// flutter/goldens repository. +/// * [FlutterSkiaGoldFileComparator], another [FlutterGoldenFileComparator] +/// that tests golden images through Skia Gold. +class FlutterSkippingGoldenFileComparator extends FlutterGoldenFileComparator { + /// Creates a [FlutterSkippingGoldenFileComparator] that will skip tests that + /// are not in the right environment for golden file testing. + FlutterSkippingGoldenFileComparator(Uri basedir) : super(basedir); + + /// Creates a new [FlutterSkippingGoldenFileComparator] that mirrors the relative + /// path resolution of the default [goldenFileComparator]. + static FlutterSkippingGoldenFileComparator fromDefaultComparator({ + LocalFileComparator defaultComparator, + }) { + defaultComparator ??= goldenFileComparator; + return FlutterSkippingGoldenFileComparator(defaultComparator.basedir); + } + + @override + Future<bool> compare(Uint8List imageBytes, Uri golden) async { + print('Skipping "$golden" test : Skia Gold is not available in this testing ' + 'environment and flutter/goldens repository comparison is only available ' + 'on Linux machines.' + ); + return true; + } + + @override + Future<void> update(Uri golden, Uint8List imageBytes) => null; }
diff --git a/packages/flutter_goldens/test/flutter_goldens_test.dart b/packages/flutter_goldens/test/flutter_goldens_test.dart index 19645ed..4f15200 100644 --- a/packages/flutter_goldens/test/flutter_goldens_test.dart +++ b/packages/flutter_goldens/test/flutter_goldens_test.dart
@@ -34,13 +34,13 @@ }); group('GoldensClient', () { - GoldensClient goldens; + GoldensRepositoryClient goldens; setUp(() { - goldens = GoldensClient( + goldens = GoldensRepositoryClient( fs: fs, - platform: platform, process: process, + platform: platform, ); }); @@ -60,32 +60,65 @@ }); }); + group('SkiaGoldClient', () { + SkiaGoldClient goldens; + + setUp(() { + goldens = SkiaGoldClient( + fs: fs, + process: process, + platform: platform, + ); + }); + + group('auth', () { + test('performs minimal work if already authorized', () async { + final Directory workDirectory = fs.directory('/workDirectory')..createSync(recursive: true); + fs.file('/workDirectory/temp/auth_opt.json')..createSync(recursive: true); + when(process.run(any)).thenAnswer((_) => Future<io.ProcessResult>.value(io.ProcessResult(123, 0, '', ''))); + await goldens.auth(workDirectory); + + // Verify that we spawned no process calls + final VerificationResult verifyProcessRun = + verifyNever(process.run(captureAny, workingDirectory: captureAnyNamed('workingDirectory'))); + expect(verifyProcessRun.callCount, 0); + }); + }); + }); + group('FlutterGoldenFileComparator', () { + test('calculates the basedir correctly', () async { + final MockSkiaGoldClient goldens = MockSkiaGoldClient(); + final MockLocalFileComparator defaultComparator = MockLocalFileComparator(); + final Directory flutterRoot = fs.directory('/foo')..createSync(recursive: true); + final Directory goldensRoot = flutterRoot.childDirectory('bar')..createSync(recursive: true); + when(goldens.fs).thenReturn(fs); + when(goldens.flutterRoot).thenReturn(flutterRoot); + when(goldens.comparisonRoot).thenReturn(goldensRoot); + when(defaultComparator.basedir).thenReturn(flutterRoot.childDirectory('baz').uri); + final Directory basedir = FlutterGoldenFileComparator.getBaseDirectory(goldens, defaultComparator); + expect(basedir.uri, fs.directory('/foo/bar/baz').uri); + }); + }); + + group('FlutterGoldensRepositoryFileComparator', () { MemoryFileSystem fs; - FlutterGoldenFileComparator comparator; + FlutterGoldensRepositoryFileComparator comparator; setUp(() { fs = MemoryFileSystem(); + platform = FakePlatform( + operatingSystem: 'linux', + environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot}, + ); final Directory flutterRoot = fs.directory('/path/to/flutter')..createSync(recursive: true); final Directory goldensRoot = flutterRoot.childDirectory('bin/cache/goldens')..createSync(recursive: true); final Directory testDirectory = goldensRoot.childDirectory('test/foo/bar')..createSync(recursive: true); - comparator = FlutterGoldenFileComparator(testDirectory.uri, fs: fs); - }); - - group('fromDefaultComparator', () { - test('calculates the basedir correctly', () async { - final MockGoldensClient goldens = MockGoldensClient(); - final MockLocalFileComparator defaultComparator = MockLocalFileComparator(); - final Directory flutterRoot = fs.directory('/foo')..createSync(recursive: true); - final Directory goldensRoot = flutterRoot.childDirectory('bar')..createSync(recursive: true); - when(goldens.fs).thenReturn(fs); - when(goldens.flutterRoot).thenReturn(flutterRoot); - when(goldens.repositoryRoot).thenReturn(goldensRoot); - when(defaultComparator.basedir).thenReturn(flutterRoot.childDirectory('baz').uri); - comparator = await FlutterGoldenFileComparator.fromDefaultComparator( - goldens: goldens, defaultComparator: defaultComparator); - expect(comparator.basedir, fs.directory('/foo/bar/baz').uri); - }); + comparator = FlutterGoldensRepositoryFileComparator( + testDirectory.uri, + fs: fs, + platform: platform, + ); }); group('compare', () { @@ -132,9 +165,44 @@ expect(goldenFile.readAsBytesSync(), <int>[1, 2, 3]); }); }); + + group('getTestUri', () { + test('incorporates version number', () { + final Uri key = comparator.getTestUri(Uri.parse('foo.png'), 1); + expect(key, Uri.parse('foo.1.png')); + }); + test('ignores null version number', () { + final Uri key = comparator.getTestUri(Uri.parse('foo.png'), null); + expect(key, Uri.parse('foo.png')); + }); + }); + }); + + group('FlutterSkiaGoldFileComparator', () { + FlutterSkiaGoldFileComparator comparator; + + setUp(() { + final Directory flutterRoot = fs.directory('/path/to/flutter')..createSync(recursive: true); + final Directory goldensRoot = flutterRoot.childDirectory('bin/cache/goldens')..createSync(recursive: true); + final Directory testDirectory = goldensRoot.childDirectory('test/foo/bar')..createSync(recursive: true); + comparator = FlutterSkiaGoldFileComparator( + testDirectory.uri, + MockSkiaGoldClient(), + fs: fs, + platform: platform, + ); + }); + + group('getTestUri', () { + test('ignores version number', () { + final Uri key = comparator.getTestUri(Uri.parse('foo.png'), 1); + expect(key, Uri.parse('foo.png')); + }); + }); }); } class MockProcessManager extends Mock implements ProcessManager {} -class MockGoldensClient extends Mock implements GoldensClient {} +class MockGoldensRepositoryClient extends Mock implements GoldensRepositoryClient {} +class MockSkiaGoldClient extends Mock implements SkiaGoldClient {} class MockLocalFileComparator extends Mock implements LocalFileComparator {}
diff --git a/packages/flutter_goldens_client/lib/client.dart b/packages/flutter_goldens_client/lib/client.dart index 5725984..2a0c792 100644 --- a/packages/flutter_goldens_client/lib/client.dart +++ b/packages/flutter_goldens_client/lib/client.dart
@@ -16,11 +16,11 @@ const String _kFlutterRootKey = 'FLUTTER_ROOT'; -/// A class that represents a clone of the https://github.com/flutter/goldens -/// repository, nested within the `bin/cache` directory of the caller's Flutter -/// repository. -class GoldensClient { - /// Create a handle to a local clone of the goldens repository. +/// A base class that provides shared information to the +/// [FlutterGoldenFileComparator] as well as the [SkiaGoldClient] and +/// [GoldensRepositoryClient]. +abstract class GoldensClient { + /// Creates a handle to the local environment of golden file images. GoldensClient({ this.fs = const LocalFileSystem(), this.platform = const LocalPlatform(), @@ -46,17 +46,33 @@ /// subprocesses. final ProcessManager process; - RandomAccessFile _lock; - /// The local [Directory] where the Flutter repository is hosted. /// /// Uses the [fs] file system. Directory get flutterRoot => fs.directory(platform.environment[_kFlutterRootKey]); - /// The local [Directory] where the goldens repository is hosted. + /// The local [Directory] where the goldens files are located. /// /// Uses the [fs] file system. - Directory get repositoryRoot => flutterRoot.childDirectory(fs.path.join('bin', 'cache', 'pkg', 'goldens')); + Directory get comparisonRoot => flutterRoot.childDirectory(fs.path.join('bin', 'cache', 'pkg', 'goldens')); + +} + +/// A class that represents a clone of the https://github.com/flutter/goldens +/// repository, nested within the `bin/cache` directory of the caller's Flutter +/// repository. +class GoldensRepositoryClient extends GoldensClient { + GoldensRepositoryClient({ + FileSystem fs = const LocalFileSystem(), + ProcessManager process = const LocalProcessManager(), + Platform platform = const LocalPlatform(), + }) : super( + fs: fs, + process: process, + platform: platform, + ); + + RandomAccessFile _lock; /// Prepares the local clone of the `flutter/goldens` repository for golden /// file testing. @@ -89,45 +105,45 @@ } } - Future<String> _getGoldensCommit() async { - final File versionFile = flutterRoot.childFile(fs.path.join('bin', 'internal', 'goldens.version')); - return (await versionFile.readAsString()).trim(); - } - Future<String> _getCurrentCommit() async { - if (!repositoryRoot.existsSync()) { + if (!comparisonRoot.existsSync()) { return null; } else { final io.ProcessResult revParse = await process.run( <String>['git', 'rev-parse', 'HEAD'], - workingDirectory: repositoryRoot.path, + workingDirectory: comparisonRoot.path, ); return revParse.exitCode == 0 ? revParse.stdout.trim() : null; } } + Future<String> _getGoldensCommit() async { + final File versionFile = flutterRoot.childFile(fs.path.join('bin', 'internal', 'goldens.version')); + return (await versionFile.readAsString()).trim(); + } + Future<void> _initRepository() async { - await repositoryRoot.create(recursive: true); + await comparisonRoot.create(recursive: true); await _runCommands( <String>[ 'git init', 'git remote add upstream https://github.com/flutter/goldens.git', 'git remote set-url --push upstream git@github.com:flutter/goldens.git', ], - workingDirectory: repositoryRoot, + workingDirectory: comparisonRoot, ); } Future<void> _checkCanSync() async { final io.ProcessResult result = await process.run( <String>['git', 'status', '--porcelain'], - workingDirectory: repositoryRoot.path, + workingDirectory: comparisonRoot.path, ); if (result.stdout.trim().isNotEmpty) { final StringBuffer buf = StringBuffer() - ..writeln('flutter_goldens git checkout at ${repositoryRoot.path} has local changes and cannot be synced.') + ..writeln('flutter_goldens git checkout at ${comparisonRoot.path} has local changes and cannot be synced.') ..writeln('To reset your client to a clean state, and lose any local golden test changes:') - ..writeln('cd ${repositoryRoot.path}') + ..writeln('cd ${comparisonRoot.path}') ..writeln('git reset --hard HEAD') ..writeln('git clean -x -d -f -f'); throw NonZeroExitCode(1, buf.toString()); @@ -141,7 +157,7 @@ 'git fetch upstream $commit', 'git reset --hard FETCH_HEAD', ], - workingDirectory: repositoryRoot, + workingDirectory: comparisonRoot, ); } @@ -173,6 +189,7 @@ _lock = null; } } + /// Exception that signals a process' exit with a non-zero exit code. class NonZeroExitCode implements Exception { /// Create an exception that represents a non-zero exit code.
diff --git a/packages/flutter_goldens_client/lib/skia_client.dart b/packages/flutter_goldens_client/lib/skia_client.dart new file mode 100644 index 0000000..27de48c --- /dev/null +++ b/packages/flutter_goldens_client/lib/skia_client.dart
@@ -0,0 +1,222 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:path/path.dart' as path; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; + +import 'package:flutter_goldens_client/client.dart'; + +// If you are here trying to figure out how to use golden files in the Flutter +// repo itself, consider reading this wiki page: +// https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package%3Aflutter + +// TODO(Piinks): This file will replace ./client.dart when transition to Skia +// Gold testing is complete + +const String _kGoldctlKey = 'GOLDCTL'; +const String _kServiceAccountKey = 'GOLD_SERVICE_ACCOUNT'; + +/// An extension of the [GoldensClient] class that interfaces with Skia Gold +/// for golden file testing. +class SkiaGoldClient extends GoldensClient { + SkiaGoldClient({ + FileSystem fs = const LocalFileSystem(), + ProcessManager process = const LocalProcessManager(), + Platform platform = const LocalPlatform(), + }) : super( + fs: fs, + process: process, + platform: platform, + ); + + /// The local [Directory] within the [comparisonRoot] for the current test + /// context. In this directory, the client will create image and json files + /// for the goldctl tool to use. + /// + /// This is informed by the [FlutterGoldenFileComparator] [basedir]. It cannot + /// be null. + Directory _workDirectory; + + /// The path to the local [Directory] where the goldctl tool is hosted. + /// + /// Uses the [platform] environment in this implementation. + String get _goldctl => platform.environment[_kGoldctlKey]; + + /// The path to the local [Directory] where the service account key is + /// hosted. + /// + /// Uses the [platform] environment in this implementation. + String get _serviceAccount => platform.environment[_kServiceAccountKey]; + + @override + Directory get comparisonRoot => flutterRoot.childDirectory(fs.path.join('bin', 'cache', 'pkg', 'skia_goldens')); + + /// Prepares the local work space for golden file testing and calls the + /// goldctl `auth` command. + /// + /// This ensures that the goldctl tool is authorized and ready for testing. It + /// will only be called once for each instance of + /// [FlutterSkiaGoldFileComparator]. + /// + /// The [workDirectory] parameter specifies the current directory that golden + /// tests are executing in, relative to the library of the given test. It is + /// informed by the basedir of the [FlutterSkiaGoldFileComparator]. + Future<void> auth(Directory workDirectory) async { + assert(workDirectory != null); + _workDirectory = workDirectory; + if (_clientIsAuthorized()) + return; + + if (_serviceAccount.isEmpty) { + final StringBuffer buf = StringBuffer()..writeln('Gold service account is unavailable.'); + throw NonZeroExitCode(1, buf.toString()); + } + + final File authorization = _workDirectory.childFile('serviceAccount.json'); + await authorization.writeAsString(_serviceAccount); + + final List<String> authArguments = <String>[ + 'auth', + '--service-account', authorization.path, + '--work-dir', _workDirectory.childDirectory('temp').path, + ]; + + // final io.ProcessResult authResults = + await io.Process.run( + _goldctl, + authArguments, + ); + // TODO(Piinks): Re-enable after Gold flakes are resolved, https://github.com/flutter/flutter/pull/36103 + // if (authResults.exitCode != 0) { + // final StringBuffer buf = StringBuffer() + // ..writeln('Flutter + Skia Gold auth failed.') + // ..writeln('stdout: ${authResults.stdout}') + // ..writeln('stderr: ${authResults.stderr}'); + // throw NonZeroExitCode(authResults.exitCode, buf.toString()); + // } + } + + /// Executes the `imgtest init` command in the goldctl tool. + /// + /// The `imgtest` command collects and uploads test results to the Skia Gold + /// backend, the `init` argument initializes the current test. + Future<void> imgtestInit() async { + final File keys = _workDirectory.childFile('keys.json'); + final File failures = _workDirectory.childFile('failures.json'); + + await keys.writeAsString(_getKeysJSON()); + await failures.create(); + final String commitHash = await _getCurrentCommit(); + + final List<String> imgtestInitArguments = <String>[ + 'imgtest', 'init', + '--instance', 'flutter', + '--work-dir', _workDirectory.childDirectory('temp').path, + '--commit', commitHash, + '--keys-file', keys.path, + '--failure-file', failures.path, + '--passfail', + ]; + + if (imgtestInitArguments.contains(null)) { + final StringBuffer buf = StringBuffer(); + buf.writeln('Null argument for Skia Gold imgtest init:'); + imgtestInitArguments.forEach(buf.writeln); + throw NonZeroExitCode(1, buf.toString()); + } + + // final io.ProcessResult imgtestInitResult = + await io.Process.run( + _goldctl, + imgtestInitArguments, + ); + + // TODO(Piinks): Re-enable after Gold flakes are resolved, https://github.com/flutter/flutter/pull/36103 + // if (imgtestInitResult.exitCode != 0) { + // final StringBuffer buf = StringBuffer() + // ..writeln('Flutter + Skia Gold imgtest init failed.') + // ..writeln('stdout: ${imgtestInitResult.stdout}') + // ..writeln('stderr: ${imgtestInitResult.stderr}'); + // throw NonZeroExitCode(imgtestInitResult.exitCode, buf.toString()); + // } + } + + /// Executes the `imgtest add` command in the goldctl tool. + /// + /// The `imgtest` command collects and uploads test results to the Skia Gold + /// backend, the `add` argument uploads the current image test. A response is + /// returned from the invocation of this command that indicates a pass or fail + /// result. + /// + /// The testName and goldenFile parameters reference the current comparison + /// being evaluated by the [FlutterSkiaGoldFileComparator]. + Future<bool> imgtestAdd(String testName, File goldenFile) async { + assert(testName != null); + assert(goldenFile != null); + + final List<String> imgtestArguments = <String>[ + 'imgtest', 'add', + '--work-dir', _workDirectory.childDirectory('temp').path, + '--test-name', testName.split(path.extension(testName.toString()))[0], + '--png-file', goldenFile.path, + ]; + + await io.Process.run( + _goldctl, + imgtestArguments, + ); + + // TODO(Piinks): Comment on PR if triage is needed, https://github.com/flutter/flutter/issues/34673 + // So as not to turn the tree red in this initial implementation, this will + // return true for now. + // The ProcessResult that returns from line 157 contains the pass/fail + // result of the test & links to the dashboard and diffs. + return true; + } + + /// Returns the current commit hash of the Flutter repository. + Future<String> _getCurrentCommit() async { + if (!flutterRoot.existsSync()) { + final StringBuffer buf = StringBuffer() + ..writeln('Flutter root could not be found: $flutterRoot'); + throw NonZeroExitCode(1, buf.toString()); + } else { + final io.ProcessResult revParse = await process.run( + <String>['git', 'rev-parse', 'HEAD'], + workingDirectory: flutterRoot.path, + ); + return revParse.exitCode == 0 ? revParse.stdout.trim() : null; + } + } + + /// Returns a JSON String with keys value pairs used to uniquely identify the + /// configuration that generated the given golden file. + /// + /// Currently, the only key value pair being tracked is the platform the image + /// was rendered on. + String _getKeysJSON() { + return json.encode( + <String, dynamic>{ + 'Platform' : platform.operatingSystem, + } + ); + } + + /// Returns a boolean value to prevent the client from re-authorizing itself + /// for multiple tests. + bool _clientIsAuthorized() { + final File authFile = _workDirectory?.childFile(super.fs.path.join( + 'temp', + 'auth_opt.json', + )); + return authFile.existsSync(); + } +}
diff --git a/packages/flutter_test/lib/src/goldens.dart b/packages/flutter_test/lib/src/goldens.dart index 6672755..1bc6fcd 100644 --- a/packages/flutter_test/lib/src/goldens.dart +++ b/packages/flutter_test/lib/src/goldens.dart
@@ -45,6 +45,26 @@ /// The method by which [golden] is located and by which its bytes are written /// is left up to the implementation class. Future<void> update(Uri golden, Uint8List imageBytes); + + /// Returns a new golden file [Uri] to incorporate any [version] number with + /// the [key]. + /// + /// The [version] is an optional int that can be used to differentiate + /// historical golden files. + /// + /// Version numbers are used in golden file tests for package:flutter. You can + /// learn more about these tests [here](https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter). + Uri getTestUri(Uri key, int version) { + if (version == null) + return key; + final String keyString = key.toString(); + final String extension = path.extension(keyString); + return Uri.parse( + keyString + .split(extension) + .join() + '.' + version.toString() + extension + ); + } } /// Compares rasterized image bytes against a golden image file. @@ -126,6 +146,11 @@ Future<void> update(Uri golden, Uint8List imageBytes) { throw StateError('goldenFileComparator has not been initialized'); } + + @override + Uri getTestUri(Uri key, int version) { + return key; + } } /// The default [GoldenFileComparator] implementation for `flutter test`. @@ -140,7 +165,7 @@ /// /// When using `flutter test --update-goldens`, [LocalFileComparator] /// updates the files on disk to match the rendering. -class LocalFileComparator implements GoldenFileComparator { +class LocalFileComparator extends GoldenFileComparator { /// Creates a new [LocalFileComparator] for the specified [testFile]. /// /// Golden file keys will be interpreted as file paths relative to the
diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index 73494da..8e3dca3 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart
@@ -1715,7 +1715,7 @@ imageFuture = _captureImage(elements.single); } - final Uri testNameUri = _getTestNameUri(key, version); + final Uri testNameUri = goldenFileComparator.getTestUri(key, version); final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); return binding.runAsync<String>(() async { @@ -1737,19 +1737,9 @@ } @override - Description describe(Description description) => - description.add('one widget whose rasterized image matches golden image "${_getTestNameUri(key, version)}"'); - - Uri _getTestNameUri(Uri key, int version) { - return version == null ? key : Uri.parse( - key - .toString() - .splitMapJoin( - RegExp(r'.png'), - onMatch: (Match m) => '${'.' + version.toString() + m.group(0)}', - onNonMatch: (String n) => '$n' - ) - ); + Description describe(Description description) { + final Uri testNameUri = goldenFileComparator.getTestUri(key, version); + return description.add('one widget whose rasterized image matches golden image "$testNameUri"'); } }
diff --git a/packages/flutter_test/test/goldens_test.dart b/packages/flutter_test/test/goldens_test.dart index 5d03642..004b336 100644 --- a/packages/flutter_test/test/goldens_test.dart +++ b/packages/flutter_test/test/goldens_test.dart
@@ -182,5 +182,18 @@ expect(fs.file(fix('/foo.png')).readAsBytesSync(), newBytes); }); }); + + group('getTestUri', () { + test('updates file name with version number', () { + final Uri key = Uri.parse('foo.png'); + final Uri key1 = comparator.getTestUri(key, 1); + expect(key1, Uri.parse('foo.1.png')); + }); + test('does nothing for null version number', () { + final Uri key = Uri.parse('foo.png'); + final Uri keyNull = comparator.getTestUri(key, null); + expect(keyNull, Uri.parse('foo.png')); + }); + }); }); }
diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index 5fb64f3..f51bd48 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart
@@ -335,30 +335,6 @@ expect(comparator.imageBytes, hasLength(greaterThan(0))); expect(comparator.golden, Uri.parse('foo.png')); }); - - testWidgets('Comparator succeeds incorporating version number', (WidgetTester tester) async { - await tester.pumpWidget(boilerplate(const Text('hello'))); - final Finder finder = find.byType(Text); - await expectLater(finder, matchesGoldenFile( - 'foo.png', - version: 1, - )); - expect(comparator.invocation, _ComparatorInvocation.compare); - expect(comparator.imageBytes, hasLength(greaterThan(0))); - expect(comparator.golden, Uri.parse('foo.1.png')); - }); - - testWidgets('Comparator succeeds with null version number', (WidgetTester tester) async { - await tester.pumpWidget(boilerplate(const Text('hello'))); - final Finder finder = find.byType(Text); - await expectLater(finder, matchesGoldenFile( - 'foo.png', - version: null, - )); - expect(comparator.invocation, _ComparatorInvocation.compare); - expect(comparator.imageBytes, hasLength(greaterThan(0))); - expect(comparator.golden, Uri.parse('foo.png')); - }); }); group('does not match', () { @@ -413,40 +389,6 @@ expect(error.message, contains('too many widgets')); } }); - - testWidgets('Comparator failure incorporates version number', (WidgetTester tester) async { - comparator.behavior = _ComparatorBehavior.returnFalse; - await tester.pumpWidget(boilerplate(const Text('hello'))); - final Finder finder = find.byType(Text); - try { - await expectLater(finder, matchesGoldenFile( - 'foo.png', - version: 1, - )); - fail('TestFailure expected but not thrown'); - } on TestFailure catch (error) { - expect(comparator.invocation, _ComparatorInvocation.compare); - expect(error.message, contains('does not match')); - expect(error.message, contains('foo.1.png')); - } - }); - - testWidgets('Comparator failure with null version number', (WidgetTester tester) async { - comparator.behavior = _ComparatorBehavior.returnFalse; - await tester.pumpWidget(boilerplate(const Text('hello'))); - final Finder finder = find.byType(Text); - try { - await expectLater(finder, matchesGoldenFile( - 'foo.png', - version: null, - )); - fail('TestFailure expected but not thrown'); - } on TestFailure catch (error) { - expect(comparator.invocation, _ComparatorInvocation.compare); - expect(error.message, contains('does not match')); - expect(error.message, contains('foo.png')); - } - }); }); testWidgets('calls update on comparator if autoUpdateGoldenFiles is true', (WidgetTester tester) async { @@ -712,6 +654,11 @@ this.imageBytes = imageBytes; return Future<void>.value(); } + + @override + Uri getTestUri(Uri key, int version) { + return key; + } } class _FakeSemanticsNode extends SemanticsNode {