mirror of
https://github.com/flutter/flutter
synced 2024-10-12 19:23:02 +00:00
This reverts commit 8df0d6556d
.
This commit is contained in:
parent
22f0bf87e4
commit
3a3939a7fa
1
bin/internal/goldens.version
Normal file
1
bin/internal/goldens.version
Normal file
|
@ -0,0 +1 @@
|
|||
65bd07204149c4f7612bbf179cf088a2d69ca549
|
|
@ -45,7 +45,7 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(key),
|
||||
matchesGoldenFile('activityIndicator.paused.light.png'),
|
||||
matchesGoldenFile('activityIndicator.paused.light.png', version: 0),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
|
@ -65,7 +65,7 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(key),
|
||||
matchesGoldenFile('activityIndicator.paused.dark.png'),
|
||||
matchesGoldenFile('activityIndicator.paused.dark.png', version: 0),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -922,7 +922,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(CupertinoDatePicker),
|
||||
matchesGoldenFile('date_picker_test.datetime.initial.png'),
|
||||
matchesGoldenFile(
|
||||
'date_picker_test.datetime.initial.png',
|
||||
version: 2,
|
||||
),
|
||||
);
|
||||
|
||||
// Slightly drag the hour component to make the current hour off-center.
|
||||
|
@ -931,7 +934,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(CupertinoDatePicker),
|
||||
matchesGoldenFile('date_picker_test.datetime.drag.png'),
|
||||
matchesGoldenFile(
|
||||
'date_picker_test.datetime.drag.png',
|
||||
version: 2,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -965,7 +971,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(CupertinoTimerPicker),
|
||||
matchesGoldenFile('timer_picker_test.datetime.initial.png'),
|
||||
matchesGoldenFile(
|
||||
'timer_picker_test.datetime.initial.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
|
||||
// Slightly drag the minute component to make the current minute off-center.
|
||||
|
@ -974,7 +983,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(CupertinoTimerPicker),
|
||||
matchesGoldenFile('timer_picker_test.datetime.drag.png'),
|
||||
matchesGoldenFile(
|
||||
'timer_picker_test.datetime.drag.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -820,7 +820,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).last,
|
||||
matchesGoldenFile('nav_bar_test.standard_title.png'),
|
||||
matchesGoldenFile(
|
||||
'nav_bar_test.standard_title.png',
|
||||
version: 2,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -851,7 +854,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).last,
|
||||
matchesGoldenFile('nav_bar_test.large_title.png'),
|
||||
matchesGoldenFile(
|
||||
'nav_bar_test.large_title.png',
|
||||
version: 2,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1414,7 +1414,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary),
|
||||
matchesGoldenFile('segmented_control_test.0.png'),
|
||||
matchesGoldenFile(
|
||||
'segmented_control_test.0.png',
|
||||
version: 0,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1452,7 +1455,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary),
|
||||
matchesGoldenFile('segmented_control_test.1.png'),
|
||||
matchesGoldenFile(
|
||||
'segmented_control_test.1.png',
|
||||
version: 0,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -542,7 +542,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(switchKey),
|
||||
matchesGoldenFile('switch.tap.off.png'),
|
||||
matchesGoldenFile(
|
||||
'switch.tap.off.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(switchKey));
|
||||
|
@ -553,13 +556,19 @@ void main() {
|
|||
await tester.pump(const Duration(milliseconds: 60));
|
||||
await expectLater(
|
||||
find.byKey(switchKey),
|
||||
matchesGoldenFile('switch.tap.turningOn.png'),
|
||||
matchesGoldenFile(
|
||||
'switch.tap.turningOn.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
await expectLater(
|
||||
find.byKey(switchKey),
|
||||
matchesGoldenFile('switch.tap.on.png'),
|
||||
matchesGoldenFile(
|
||||
'switch.tap.on.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -595,7 +604,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(switchKey),
|
||||
matchesGoldenFile('switch.tap.off.dark.png'),
|
||||
matchesGoldenFile(
|
||||
'switch.tap.off.dark.png',
|
||||
version: 0,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(switchKey));
|
||||
|
@ -604,7 +616,10 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
await expectLater(
|
||||
find.byKey(switchKey),
|
||||
matchesGoldenFile('switch.tap.on.dark.png'),
|
||||
matchesGoldenFile(
|
||||
'switch.tap.on.dark.png',
|
||||
version: 0,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -503,7 +503,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(const ValueKey<int>(1)),
|
||||
matchesGoldenFile('text_field_cursor_test.cupertino.0.png'),
|
||||
matchesGoldenFile(
|
||||
'text_field_cursor_test.cupertino.0.png',
|
||||
version: 3,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -533,7 +536,10 @@ void main() {
|
|||
debugDefaultTargetPlatformOverride = null;
|
||||
await expectLater(
|
||||
find.byKey(const ValueKey<int>(1)),
|
||||
matchesGoldenFile('text_field_cursor_test.cupertino.1.png'),
|
||||
matchesGoldenFile(
|
||||
'text_field_cursor_test.cupertino.1.png',
|
||||
version: 3,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -3036,7 +3042,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(const ValueKey<int>(1)),
|
||||
matchesGoldenFile('text_field_test.disabled.png'),
|
||||
matchesGoldenFile(
|
||||
'text_field_test.disabled.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -71,13 +71,19 @@ void main() {
|
|||
await pump(FloatingActionButtonLocation.endDocked);
|
||||
await expectLater(
|
||||
find.byKey(key),
|
||||
matchesGoldenFile('bottom_app_bar.custom_shape.1.png'),
|
||||
matchesGoldenFile(
|
||||
'bottom_app_bar.custom_shape.1.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
await pump(FloatingActionButtonLocation.centerDocked);
|
||||
await tester.pumpAndSettle();
|
||||
await expectLater(
|
||||
find.byKey(key),
|
||||
matchesGoldenFile('bottom_app_bar.custom_shape.2.png'),
|
||||
matchesGoldenFile(
|
||||
'bottom_app_bar.custom_shape.2.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
|
|
@ -80,7 +80,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(_painterKey),
|
||||
matchesGoldenFile('bottom_app_bar_theme.custom_shape.png'),
|
||||
matchesGoldenFile(
|
||||
'bottom_app_bar_theme.custom_shape.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
|
|
@ -1426,7 +1426,10 @@ void main() {
|
|||
await tester.pump(const Duration(milliseconds: 30));
|
||||
await expectLater(
|
||||
find.byType(BottomNavigationBar),
|
||||
matchesGoldenFile('bottom_navigation_bar.shifting_transition.$pump.png'),
|
||||
matchesGoldenFile(
|
||||
'bottom_navigation_bar.shifting_transition.$pump.png',
|
||||
version: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, skip: isBrowser);
|
||||
|
|
|
@ -137,7 +137,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(painterKey),
|
||||
matchesGoldenFile('card_theme.custom_shape.png'),
|
||||
matchesGoldenFile(
|
||||
'card_theme.custom_shape.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
}
|
||||
|
|
|
@ -130,7 +130,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(_painterKey),
|
||||
matchesGoldenFile('dialog_theme.dialog_with_custom_border.png'),
|
||||
matchesGoldenFile(
|
||||
'dialog_theme.dialog_with_custom_border.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
|
|
@ -227,7 +227,10 @@ void main() {
|
|||
assert(tester.renderObject(buttonFinder).attached);
|
||||
await expectLater(
|
||||
find.ancestor(of: buttonFinder, matching: find.byType(RepaintBoundary)).first,
|
||||
matchesGoldenFile('dropdown_test.default.png'),
|
||||
matchesGoldenFile(
|
||||
'dropdown_test.default.png',
|
||||
version: 0,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -239,7 +242,10 @@ void main() {
|
|||
assert(tester.renderObject(buttonFinder).attached);
|
||||
await expectLater(
|
||||
find.ancestor(of: buttonFinder, matching: find.byType(RepaintBoundary)).first,
|
||||
matchesGoldenFile('dropdown_test.expanded.png'),
|
||||
matchesGoldenFile(
|
||||
'dropdown_test.expanded.png',
|
||||
version: 0,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
|
|
@ -740,7 +740,10 @@ void main() {
|
|||
await tester.pump(const Duration(milliseconds: 1000));
|
||||
await expectLater(
|
||||
find.byKey(key),
|
||||
matchesGoldenFile('floating_action_button_test.clip.png'),
|
||||
matchesGoldenFile(
|
||||
'floating_action_button_test.clip.png',
|
||||
version: 2,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -2960,13 +2960,19 @@ void main() {
|
|||
await tester.pumpWidget(buildFrame(TextDirection.ltr));
|
||||
await expectLater(
|
||||
find.byType(InputDecorator),
|
||||
matchesGoldenFile('input_decorator.outline_icon_label.ltr.png'),
|
||||
matchesGoldenFile(
|
||||
'input_decorator.outline_icon_label.ltr.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(buildFrame(TextDirection.rtl));
|
||||
await expectLater(
|
||||
find.byType(InputDecorator),
|
||||
matchesGoldenFile('input_decorator.outline_icon_label.rtl.png'),
|
||||
matchesGoldenFile(
|
||||
'input_decorator.outline_icon_label.rtl.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -715,7 +715,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(painterKey),
|
||||
matchesGoldenFile('material.border_paint_above.png'),
|
||||
matchesGoldenFile(
|
||||
'material.border_paint_above.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -755,7 +758,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(painterKey),
|
||||
matchesGoldenFile('material.border_paint_below.png'),
|
||||
matchesGoldenFile(
|
||||
'material.border_paint_below.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
});
|
||||
|
|
|
@ -276,7 +276,10 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
await expectLater(
|
||||
find.byKey(painterKey),
|
||||
matchesGoldenFile('radio.ink_ripple.png'),
|
||||
matchesGoldenFile(
|
||||
'radio.ink_ripple.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
}
|
||||
|
|
|
@ -267,7 +267,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(_painterKey),
|
||||
matchesGoldenFile('tab_bar_theme.tab_indicator_size_tab.png'),
|
||||
matchesGoldenFile(
|
||||
'tab_bar_theme.tab_indicator_size_tab.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -278,7 +281,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(_painterKey),
|
||||
matchesGoldenFile('tab_bar_theme.tab_indicator_size_label.png'),
|
||||
matchesGoldenFile(
|
||||
'tab_bar_theme.tab_indicator_size_label.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -294,7 +300,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(_painterKey),
|
||||
matchesGoldenFile('tab_bar_theme.custom_tab_indicator.png'),
|
||||
matchesGoldenFile(
|
||||
'tab_bar_theme.custom_tab_indicator.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -310,7 +319,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(_painterKey),
|
||||
matchesGoldenFile('tab_bar_theme.beveled_rect_indicator.png'),
|
||||
matchesGoldenFile(
|
||||
'tab_bar_theme.beveled_rect_indicator.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
}
|
||||
|
|
|
@ -412,7 +412,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(const ValueKey<int>(1)),
|
||||
matchesGoldenFile('text_field_cursor_test.material.0.png'),
|
||||
matchesGoldenFile(
|
||||
'text_field_cursor_test.material.0.png',
|
||||
version: 0,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -441,7 +444,10 @@ void main() {
|
|||
debugDefaultTargetPlatformOverride = null;
|
||||
await expectLater(
|
||||
find.byKey(const ValueKey<int>(1)),
|
||||
matchesGoldenFile('text_field_cursor_test.material.1.png'),
|
||||
matchesGoldenFile(
|
||||
'text_field_cursor_test.material.1.png',
|
||||
version: 0,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -492,7 +498,10 @@ void main() {
|
|||
await expectLater(
|
||||
// The toolbar exists in the Overlay above the MaterialApp.
|
||||
find.byType(Overlay),
|
||||
matchesGoldenFile('text_field_opacity_test.0.png'),
|
||||
matchesGoldenFile(
|
||||
'text_field_opacity_test.0.png',
|
||||
version: 3,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
|
|
@ -71,7 +71,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary),
|
||||
matchesGoldenFile('continuous_rectangle_border.golden_test_even_radii.png'),
|
||||
matchesGoldenFile(
|
||||
'continuous_rectangle_border.golden_test_even_radii.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -92,7 +95,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary),
|
||||
matchesGoldenFile('continuous_rectangle_border.golden_test_varying_radii.png'),
|
||||
matchesGoldenFile(
|
||||
'continuous_rectangle_border.golden_test_varying_radii.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -110,7 +116,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary),
|
||||
matchesGoldenFile('continuous_rectangle_border.golden_test_large_radii.png'),
|
||||
matchesGoldenFile(
|
||||
'continuous_rectangle_border.golden_test_large_radii.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
|
|
@ -806,10 +806,7 @@ void main() {
|
|||
),
|
||||
),
|
||||
));
|
||||
await expectLater(
|
||||
find.byKey(painterKey),
|
||||
matchesGoldenFile(goldenName),
|
||||
);
|
||||
await expectLater(find.byKey(painterKey), matchesGoldenFile(goldenName));
|
||||
}
|
||||
|
||||
testWidgets('Gradients - 45 degrees', (WidgetTester tester) async {
|
||||
|
|
|
@ -49,7 +49,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RichText),
|
||||
matchesGoldenFile('localized_fonts.rich_text.styled_text_span.png'),
|
||||
matchesGoldenFile(
|
||||
'localized_fonts.rich_text.styled_text_span.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
skip: isBrowser, // TODO(yjbanov): implement goldens on the Web: https://github.com/flutter/flutter/issues/40297
|
||||
|
@ -101,7 +104,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(Row),
|
||||
matchesGoldenFile('localized_fonts.text_ambient_locale.chars.png'),
|
||||
matchesGoldenFile(
|
||||
'localized_fonts.text_ambient_locale.chars.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
skip: isBrowser, // TODO(yjbanov): implement goldens on the Web: https://github.com/flutter/flutter/issues/40297
|
||||
|
@ -145,7 +151,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(Row),
|
||||
matchesGoldenFile('localized_fonts.text_explicit_locale.chars.png'),
|
||||
matchesGoldenFile(
|
||||
'localized_fonts.text_explicit_locale.chars.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
skip: isBrowser, // TODO(yjbanov): implement goldens on the Web: https://github.com/flutter/flutter/issues/40297
|
||||
|
|
|
@ -43,7 +43,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('backdrop_filter_test.cull_rect.png'),
|
||||
matchesGoldenFile(
|
||||
'backdrop_filter_test.cull_rect.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
}
|
||||
|
|
|
@ -375,7 +375,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('clip.ClipRect.png'),
|
||||
matchesGoldenFile(
|
||||
'clip.ClipRect.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -415,7 +418,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('clip.ClipRectOverlay.png'),
|
||||
matchesGoldenFile(
|
||||
'clip.ClipRectOverlay.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -464,7 +470,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('clip.ClipRRect.png'),
|
||||
matchesGoldenFile(
|
||||
'clip.ClipRRect.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -507,7 +516,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('clip.ClipOval.png'),
|
||||
matchesGoldenFile(
|
||||
'clip.ClipOval.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -555,7 +567,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('clip.ClipPath.png'),
|
||||
matchesGoldenFile(
|
||||
'clip.ClipPath.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -600,7 +615,10 @@ void main() {
|
|||
await tester.pumpWidget(genPhysicalModel(Clip.antiAlias));
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('clip.PhysicalModel.antiAlias.png'),
|
||||
matchesGoldenFile(
|
||||
'clip.PhysicalModel.antiAlias.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -608,7 +626,10 @@ void main() {
|
|||
await tester.pumpWidget(genPhysicalModel(Clip.hardEdge));
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('clip.PhysicalModel.hardEdge.png'),
|
||||
matchesGoldenFile(
|
||||
'clip.PhysicalModel.hardEdge.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -618,7 +639,10 @@ void main() {
|
|||
await tester.pumpWidget(genPhysicalModel(Clip.antiAliasWithSaveLayer));
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('clip.PhysicalModel.antiAliasWithSaveLayer.png'),
|
||||
matchesGoldenFile(
|
||||
'clip.PhysicalModel.antiAliasWithSaveLayer.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -660,7 +684,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('clip.PhysicalModel.default.png'),
|
||||
matchesGoldenFile(
|
||||
'clip.PhysicalModel.default.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -709,7 +736,10 @@ void main() {
|
|||
await tester.pumpWidget(genPhysicalShape(Clip.antiAlias));
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('clip.PhysicalShape.antiAlias.png'),
|
||||
matchesGoldenFile(
|
||||
'clip.PhysicalShape.antiAlias.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -717,7 +747,10 @@ void main() {
|
|||
await tester.pumpWidget(genPhysicalShape(Clip.hardEdge));
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('clip.PhysicalShape.hardEdge.png'),
|
||||
matchesGoldenFile(
|
||||
'clip.PhysicalShape.hardEdge.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -725,7 +758,10 @@ void main() {
|
|||
await tester.pumpWidget(genPhysicalShape(Clip.antiAliasWithSaveLayer));
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('clip.PhysicalShape.antiAliasWithSaveLayer.png'),
|
||||
matchesGoldenFile(
|
||||
'clip.PhysicalShape.antiAliasWithSaveLayer.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -771,7 +807,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('clip.PhysicalShape.default.png'),
|
||||
matchesGoldenFile(
|
||||
'clip.PhysicalShape.default.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
|
|
@ -18,7 +18,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(ColorFiltered),
|
||||
matchesGoldenFile('color_filter_red.png'),
|
||||
matchesGoldenFile(
|
||||
'color_filter_red.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -55,7 +58,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(ColorFiltered),
|
||||
matchesGoldenFile('color_filter_sepia.png'),
|
||||
matchesGoldenFile(
|
||||
'color_filter_sepia.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -90,7 +90,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(const ValueKey<int>(1)),
|
||||
matchesGoldenFile('editable_text_test.0.png'),
|
||||
matchesGoldenFile(
|
||||
'editable_text_test.0.png',
|
||||
version: 3,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -141,7 +144,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(const ValueKey<int>(1)),
|
||||
matchesGoldenFile('editable_text_test.1.png'),
|
||||
matchesGoldenFile(
|
||||
'editable_text_test.1.png',
|
||||
version: 3,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -791,7 +797,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(const ValueKey<int>(1)),
|
||||
matchesGoldenFile('editable_text_test.2.png'),
|
||||
matchesGoldenFile(
|
||||
'editable_text_test.2.png',
|
||||
version: 0,
|
||||
),
|
||||
);
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
|
|
@ -20,7 +20,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary),
|
||||
matchesGoldenFile('invert_colors_test.0.png'),
|
||||
matchesGoldenFile(
|
||||
'invert_colors_test.0.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -38,7 +41,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary),
|
||||
matchesGoldenFile('invert_colors_test.1.png'),
|
||||
matchesGoldenFile(
|
||||
'invert_colors_test.1.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
}
|
||||
|
|
|
@ -535,7 +535,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(const Key('list_wheel_scroll_view')),
|
||||
matchesGoldenFile('list_wheel_scroll_view.center_child.magnified.png'),
|
||||
matchesGoldenFile(
|
||||
'list_wheel_scroll_view.center_child.magnified.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -589,7 +592,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(const Key('list_wheel_scroll_view')),
|
||||
matchesGoldenFile('list_wheel_scroll_view.curved_wheel.left.png'),
|
||||
matchesGoldenFile(
|
||||
'list_wheel_scroll_view.curved_wheel.left.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
|
|
@ -178,7 +178,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('opacity_test.offset.png'),
|
||||
matchesGoldenFile(
|
||||
'opacity_test.offset.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
|
|
@ -110,7 +110,10 @@ void main() {
|
|||
expect(exception.diagnostics.first.toString(), startsWith('A RenderFlex overflowed by '));
|
||||
await expectLater(
|
||||
find.byKey(key),
|
||||
matchesGoldenFile('physical_model_overflow.png'),
|
||||
matchesGoldenFile(
|
||||
'physical_model_overflow.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
|
|
@ -23,14 +23,20 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('shadow.BoxDecoration.disabled.png'),
|
||||
matchesGoldenFile(
|
||||
'shadow.BoxDecoration.disabled.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
debugDisableShadows = false;
|
||||
tester.binding.reassembleApplication();
|
||||
await tester.pump();
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('shadow.BoxDecoration.enabled.png'),
|
||||
matchesGoldenFile(
|
||||
'shadow.BoxDecoration.enabled.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
debugDisableShadows = true;
|
||||
}, skip: isBrowser);
|
||||
|
@ -56,7 +62,10 @@ void main() {
|
|||
await tester.pumpWidget(build(elevation));
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('shadow.ShapeDecoration.$elevation.png'),
|
||||
matchesGoldenFile(
|
||||
'shadow.ShapeDecoration.$elevation.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
debugDisableShadows = true;
|
||||
|
@ -83,14 +92,20 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('shadow.PhysicalModel.disabled.png'),
|
||||
matchesGoldenFile(
|
||||
'shadow.PhysicalModel.disabled.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
debugDisableShadows = false;
|
||||
tester.binding.reassembleApplication();
|
||||
await tester.pump();
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('shadow.PhysicalModel.enabled.png'),
|
||||
matchesGoldenFile(
|
||||
'shadow.PhysicalModel.enabled.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
debugDisableShadows = true;
|
||||
}, skip: isBrowser);
|
||||
|
@ -120,7 +135,10 @@ void main() {
|
|||
await tester.pumpWidget(build(elevation.toDouble()));
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('shadow.PhysicalShape.$elevation.png'),
|
||||
matchesGoldenFile(
|
||||
'shadow.PhysicalShape.$elevation.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
debugDisableShadows = true;
|
||||
|
|
|
@ -30,7 +30,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.Centered.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.Centered.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
|
@ -54,7 +57,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.Centered.wrap.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.Centered.wrap.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -85,7 +91,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary),
|
||||
matchesGoldenFile('text_golden.Foreground.gradient.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.Foreground.gradient.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
|
@ -107,7 +116,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary),
|
||||
matchesGoldenFile('text_golden.Foreground.stroke.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.Foreground.stroke.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
|
@ -130,7 +142,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary),
|
||||
matchesGoldenFile('text_golden.Foreground.stroke_and_gradient.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.Foreground.stroke_and_gradient.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -180,7 +195,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary),
|
||||
matchesGoldenFile('text_golden.Background.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.Background.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -216,7 +234,10 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundary).first,
|
||||
matchesGoldenFile('text_golden.Fade.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.Fade.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -241,7 +262,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.StrutDefault.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.StrutDefault.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -268,7 +292,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.Strut.1.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.Strut.1.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -296,7 +323,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.Strut.2.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.Strut.2.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -347,7 +377,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.Strut.3.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.Strut.3.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -382,7 +415,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.Strut.4.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.Strut.4.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -433,7 +469,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.StrutForce.1.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.StrutForce.1.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -471,7 +510,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.Decoration.1.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.Decoration.1.png',
|
||||
version: 0,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -510,7 +552,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.DecorationThickness.1.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.DecorationThickness.1.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -604,7 +649,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidget.1.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.TextInlineWidget.1.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -649,7 +697,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidget.2.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.TextInlineWidget.2.png',
|
||||
version: 2,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -778,7 +829,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidgetNest.1.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.TextInlineWidgetNest.1.png',
|
||||
version: 3,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -885,7 +939,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidgetBaseline.1.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.TextInlineWidgetBaseline.1.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -992,7 +1049,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidgetAboveBaseline.1.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.TextInlineWidgetAboveBaseline.1.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1099,7 +1159,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidgetBelowBaseline.1.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.TextInlineWidgetBelowBaseline.1.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1206,7 +1269,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidgetTop.1.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.TextInlineWidgetTop.1.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1313,7 +1379,10 @@ void main() {
|
|||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidgetMiddle.1.png'),
|
||||
matchesGoldenFile(
|
||||
'text_golden.TextInlineWidgetMiddle.1.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2039,7 +2039,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
expect(expectedChildLayerCount, equals(2));
|
||||
await expectLater(
|
||||
layer.toImage(renderObject.semanticBounds.inflate(50.0)),
|
||||
matchesGoldenFile('inspector.repaint_boundary_margin.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.repaint_boundary_margin.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
// Regression test for how rendering with a pixel scale other than 1.0
|
||||
|
@ -2049,7 +2052,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
renderObject.semanticBounds.inflate(50.0),
|
||||
pixelRatio: 0.5,
|
||||
),
|
||||
matchesGoldenFile('inspector.repaint_boundary_margin_small.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.repaint_boundary_margin_small.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
|
@ -2057,7 +2063,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
renderObject.semanticBounds.inflate(50.0),
|
||||
pixelRatio: 2.0,
|
||||
),
|
||||
matchesGoldenFile('inspector.repaint_boundary_margin_large.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.repaint_boundary_margin_large.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
final Layer layerParent = layer.parent;
|
||||
|
@ -2072,7 +2081,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
width: 300.0,
|
||||
height: 300.0,
|
||||
),
|
||||
matchesGoldenFile('inspector.repaint_boundary.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.repaint_boundary.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
// Verify that taking a screenshot didn't change the layers associated with
|
||||
|
@ -2089,7 +2101,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
height: 500.0,
|
||||
margin: 50.0,
|
||||
),
|
||||
matchesGoldenFile('inspector.repaint_boundary_margin.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.repaint_boundary_margin.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
// Verify that taking a screenshot didn't change the layers associated with
|
||||
|
@ -2109,7 +2124,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
height: 300.0,
|
||||
debugPaint: true,
|
||||
),
|
||||
matchesGoldenFile('inspector.repaint_boundary_debugPaint.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.repaint_boundary_debugPaint.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
// Verify that taking a screenshot with debug paint on did not change
|
||||
// the number of children the layer has.
|
||||
|
@ -2119,7 +2137,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
// hasn't changed the regular render of the widget.
|
||||
await expectLater(
|
||||
find.byType(RepaintBoundaryWithDebugPaint),
|
||||
matchesGoldenFile('inspector.repaint_boundary.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.repaint_boundary.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
expect(renderObject.debugLayer, equals(layer));
|
||||
|
@ -2132,7 +2153,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
width: 100.0,
|
||||
height: 100.0,
|
||||
),
|
||||
matchesGoldenFile('inspector.container.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.container.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
|
@ -2142,7 +2166,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
height: 100.0,
|
||||
debugPaint: true,
|
||||
),
|
||||
matchesGoldenFile('inspector.container_debugPaint.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.container_debugPaint.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
{
|
||||
|
@ -2162,7 +2189,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
height: 100.0,
|
||||
debugPaint: true,
|
||||
),
|
||||
matchesGoldenFile('inspector.container_debugPaint.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.container_debugPaint.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
expect(container.debugNeedsLayout, isFalse);
|
||||
}
|
||||
|
@ -2174,7 +2204,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
width: 50.0,
|
||||
height: 100.0,
|
||||
),
|
||||
matchesGoldenFile('inspector.container_small.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.container_small.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
|
@ -2184,7 +2217,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
height: 400.0,
|
||||
maxPixelRatio: 3.0,
|
||||
),
|
||||
matchesGoldenFile('inspector.container_large.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.container_large.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
// This screenshot will show the clip rect debug paint but no other
|
||||
|
@ -2196,7 +2232,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
height: 100.0,
|
||||
debugPaint: true,
|
||||
),
|
||||
matchesGoldenFile('inspector.clipRect_debugPaint.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.clipRect_debugPaint.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
final Element clipRect = find.byType(ClipRRect).evaluate().single;
|
||||
|
@ -2212,7 +2251,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
// This golden image is platform dependent due to the clip icon.
|
||||
await expectLater(
|
||||
clipRectScreenshot,
|
||||
matchesGoldenFile('inspector.clipRect_debugPaint_margin.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.clipRect_debugPaint_margin.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
// Verify we get the same image if we go through the service extension
|
||||
|
@ -2251,7 +2293,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
height: 300.0,
|
||||
debugPaint: true,
|
||||
),
|
||||
matchesGoldenFile('inspector.padding_debugPaint.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.padding_debugPaint.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
// The bounds for this box crop its rendered content.
|
||||
|
@ -2262,7 +2307,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
height: 300.0,
|
||||
debugPaint: true,
|
||||
),
|
||||
matchesGoldenFile('inspector.sizedBox_debugPaint.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.sizedBox_debugPaint.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
|
||||
// Verify that setting a margin includes the previously cropped content.
|
||||
|
@ -2274,7 +2322,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
margin: 50.0,
|
||||
debugPaint: true,
|
||||
),
|
||||
matchesGoldenFile('inspector.sizedBox_debugPaint_margin.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.sizedBox_debugPaint_margin.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -2410,7 +2461,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
|
||||
await expectLater(
|
||||
find.byKey(mainStackKey),
|
||||
matchesGoldenFile('inspector.composited_transform.only_offsets.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.composited_transform.only_offsets.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
|
@ -2419,12 +2473,18 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
width: 5000.0,
|
||||
height: 500.0,
|
||||
),
|
||||
matchesGoldenFile('inspector.composited_transform.only_offsets_follower.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.composited_transform.only_offsets_follower.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
WidgetInspectorService.instance.screenshot(find.byType(Stack).evaluate().first, width: 300.0, height: 300.0),
|
||||
matchesGoldenFile('inspector.composited_transform.only_offsets_small.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.composited_transform.only_offsets_small.png',
|
||||
version: 1,
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
|
@ -2433,7 +2493,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
width: 500.0,
|
||||
height: 500.0,
|
||||
),
|
||||
matchesGoldenFile('inspector.composited_transform.only_offsets_target.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.composited_transform.only_offsets_target.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
|
@ -2505,7 +2568,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
// screenshots of specific subtrees are reasonable.
|
||||
await expectLater(
|
||||
find.byKey(mainStackKey),
|
||||
matchesGoldenFile('inspector.composited_transform.with_rotations.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.composited_transform.with_rotations.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
|
@ -2514,7 +2580,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
width: 500.0,
|
||||
height: 500.0,
|
||||
),
|
||||
matchesGoldenFile('inspector.composited_transform.with_rotations_small.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.composited_transform.with_rotations_small.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
|
@ -2523,7 +2592,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
width: 500.0,
|
||||
height: 500.0,
|
||||
),
|
||||
matchesGoldenFile('inspector.composited_transform.with_rotations_target.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.composited_transform.with_rotations_target.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
|
@ -2532,7 +2604,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
|||
width: 500.0,
|
||||
height: 500.0,
|
||||
),
|
||||
matchesGoldenFile('inspector.composited_transform.with_rotations_follower.png'),
|
||||
matchesGoldenFile(
|
||||
'inspector.composited_transform.with_rotations_follower.png',
|
||||
version: null,
|
||||
),
|
||||
);
|
||||
|
||||
// Make sure taking screenshots hasn't modified the positions of the
|
||||
|
|
|
@ -11,93 +11,49 @@ 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';
|
||||
|
||||
// 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
|
||||
|
||||
const String _kFlutterRootKey = 'FLUTTER_ROOT';
|
||||
|
||||
/// 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. _Which_ FlutterGoldenFileComparator is
|
||||
/// instantiated is based on the current testing environment.
|
||||
Future<void> main(FutureOr<void> testMain()) async {
|
||||
const Platform platform = LocalPlatform();
|
||||
if (FlutterSkiaGoldFileComparator.isAvailableForEnvironment(platform)) {
|
||||
goldenFileComparator = await FlutterSkiaGoldFileComparator.fromDefaultComparator(platform);
|
||||
} else if (FlutterPreSubmitFileComparator.isAvailableForEnvironment(platform)) {
|
||||
goldenFileComparator = await FlutterPreSubmitFileComparator.fromDefaultComparator(platform);
|
||||
if (FlutterSkiaGoldFileComparator.isAvailableOnPlatform(platform)) {
|
||||
goldenFileComparator = await FlutterSkiaGoldFileComparator.fromDefaultComparator();
|
||||
} else if (FlutterGoldensRepositoryFileComparator.isAvailableOnPlatform(platform)) {
|
||||
goldenFileComparator = await FlutterGoldensRepositoryFileComparator.fromDefaultComparator();
|
||||
} else {
|
||||
goldenFileComparator = await FlutterLocalFileComparator.fromDefaultComparator(platform);
|
||||
goldenFileComparator = FlutterSkippingGoldenFileComparator.fromDefaultComparator();
|
||||
}
|
||||
await testMain();
|
||||
}
|
||||
|
||||
/// Abstract base class golden file comparator specific to the `flutter/flutter`
|
||||
/// repository.
|
||||
///
|
||||
/// Golden file testing for the `flutter/flutter` repository is handled by three
|
||||
/// different [FlutterGoldenFileComparator]s, depending on the current testing
|
||||
/// environment.
|
||||
///
|
||||
/// * The [FlutterSkiaGoldFileComparator] is utilized during post-submit
|
||||
/// testing, after a pull request has landed on the master branch. This
|
||||
/// comparator uses the [SkiaGoldClient] and the `goldctl` tool to upload
|
||||
/// tests to the [Flutter Gold dashboard](https://flutter-gold.skia.org).
|
||||
/// Flutter Gold manages the master golden files for the `flutter/flutter`
|
||||
/// repository.
|
||||
///
|
||||
/// * The [FlutterPreSubmitFileComparator] is utilized in pre-submit testing,
|
||||
/// before a pull request can land on the master branch. This comparator
|
||||
/// uses the [SkiaGoldClient] to request the baseline images kept by the
|
||||
/// [Flutter Gold dashboard](https://flutter-gold.skia.org). It then
|
||||
/// compares the current test image to the baseline images using the
|
||||
/// standard [GoldenFileComparator.compareLists] to detect any pixel
|
||||
/// difference. The [SkiaGoldClient] is also used here to check the active
|
||||
/// ignores from the dashboard, in order to allow intended changes to pass
|
||||
/// tests.
|
||||
///
|
||||
/// * The [FlutterLocalFileComparator] is used for any other tests run outside
|
||||
/// of the above conditions. Similar to the
|
||||
/// [FlutterPreSubmitFileComparator], this comparator will use the
|
||||
/// [SkiaGoldClient] to request baseline images from
|
||||
/// [Flutter Gold](https://flutter-gold.skia.org) and compares for the
|
||||
/// current test image. If a difference is detected, this comparator will
|
||||
/// generate failure output illustrating the found difference. If a baseline
|
||||
/// is not found for a given test image, it will consider it a new test and
|
||||
/// output the new image for verification.
|
||||
abstract class FlutterGoldenFileComparator extends GoldenFileComparator {
|
||||
/// Creates a [FlutterGoldenFileComparator] that will resolve golden file
|
||||
/// URIs relative to the specified [basedir], and retrieve golden baselines
|
||||
/// using the [skiaClient]. The [basedir] is used for writing and accessing
|
||||
/// information and files for interacting with the [skiaClient]. When testing
|
||||
/// locally, the [basedir] will also contain any diffs from failed tests, or
|
||||
/// goldens generated from newly introduced tests.
|
||||
/// URIs relative to the specified [basedir].
|
||||
///
|
||||
/// The [fs] and [platform] parameters are useful in tests, where the default
|
||||
/// file system and platform can be replaced by mock instances.
|
||||
/// 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.skiaClient, {
|
||||
this.basedir, {
|
||||
this.fs = const LocalFileSystem(),
|
||||
this.platform = const LocalPlatform(),
|
||||
}) : assert(basedir != null),
|
||||
assert(skiaClient != null),
|
||||
assert(fs != null),
|
||||
assert(platform != null);
|
||||
|
||||
/// The directory to which golden file URIs will be resolved in [compare] and
|
||||
/// [update], cannot be null.
|
||||
/// [update].
|
||||
final Uri basedir;
|
||||
|
||||
/// A client for uploading image tests and making baseline requests to the
|
||||
/// Flutter Gold Dashboard, cannot be null.
|
||||
final SkiaGoldClient skiaClient;
|
||||
|
||||
/// The file system used to perform file access.
|
||||
@visibleForTesting
|
||||
final FileSystem fs;
|
||||
|
@ -113,44 +69,97 @@ abstract class FlutterGoldenFileComparator extends GoldenFileComparator {
|
|||
await goldenFile.writeAsBytes(imageBytes, flush: true);
|
||||
}
|
||||
|
||||
@override
|
||||
Uri getTestUri(Uri key, int version) => key;
|
||||
|
||||
/// Calculate the appropriate basedir for the current test context.
|
||||
@protected
|
||||
@visibleForTesting
|
||||
static Directory getBaseDirectory(LocalFileComparator defaultComparator, Platform platform) {
|
||||
const FileSystem fs = LocalFileSystem();
|
||||
final Directory flutterRoot = fs.directory(platform.environment[_kFlutterRootKey]);
|
||||
final Directory comparisonRoot = flutterRoot.childDirectory(
|
||||
fs.path.join(
|
||||
'bin',
|
||||
'cache',
|
||||
'pkg',
|
||||
'skia_goldens',
|
||||
)
|
||||
);
|
||||
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: flutterRoot.path,
|
||||
);
|
||||
return comparisonRoot.childDirectory(testDirectoryRelativePath);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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
|
||||
/// minimum. To satisfy this requirement, this comparator retrieves the golden
|
||||
/// 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 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] 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,
|
||||
);
|
||||
|
||||
/// 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 to use.
|
||||
///
|
||||
/// The [goldens] and [defaultComparator] parameters are visible for testing
|
||||
/// purposes only.
|
||||
static Future<FlutterGoldensRepositoryFileComparator> fromDefaultComparator({
|
||||
GoldensRepositoryClient goldens,
|
||||
LocalFileComparator defaultComparator,
|
||||
}) async {
|
||||
defaultComparator ??= goldenFileComparator;
|
||||
|
||||
// Prepare the goldens repo.
|
||||
goldens ??= GoldensRepositoryClient();
|
||||
await goldens.prepare();
|
||||
|
||||
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);
|
||||
if (!goldenFile.existsSync()) {
|
||||
throw TestFailure('Could not be compared against non-existent file: "$golden"');
|
||||
}
|
||||
final List<int> goldenBytes = await goldenFile.readAsBytes();
|
||||
final ComparisonResult result = GoldenFileComparator.compareLists(imageBytes, goldenBytes);
|
||||
return result.passed;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
@ -164,53 +173,44 @@ abstract class FlutterGoldenFileComparator extends GoldenFileComparator {
|
|||
///
|
||||
/// * [GoldenFileComparator], the abstract class that
|
||||
/// [FlutterGoldenFileComparator] implements.
|
||||
/// * [FlutterPreSubmitFileComparator], another
|
||||
/// [FlutterGoldenFileComparator] that tests golden images before changes are
|
||||
/// merged into the master branch.
|
||||
/// * [FlutterLocalFileComparator], another
|
||||
/// [FlutterGoldenFileComparator] that tests golden images locally on your
|
||||
/// current machine.
|
||||
/// * [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 are useful in tests, where the default
|
||||
/// file system and platform can be replaced by mock instances.
|
||||
/// 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,
|
||||
final SkiaGoldClient skiaClient, {
|
||||
final FileSystem fs = const LocalFileSystem(),
|
||||
final Platform platform = const LocalPlatform(),
|
||||
this.skiaClient, {
|
||||
FileSystem fs = const LocalFileSystem(),
|
||||
Platform platform = const LocalPlatform(),
|
||||
}) : super(
|
||||
basedir,
|
||||
skiaClient,
|
||||
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(
|
||||
final Platform platform, {
|
||||
static Future<FlutterSkiaGoldFileComparator> fromDefaultComparator({
|
||||
SkiaGoldClient goldens,
|
||||
LocalFileComparator defaultComparator,
|
||||
}) async {
|
||||
|
||||
defaultComparator ??= goldenFileComparator;
|
||||
final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(
|
||||
defaultComparator,
|
||||
platform,
|
||||
);
|
||||
goldens ??= SkiaGoldClient();
|
||||
|
||||
if(!baseDirectory.existsSync()) {
|
||||
final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(goldens, defaultComparator);
|
||||
if (!baseDirectory.existsSync())
|
||||
baseDirectory.createSync(recursive: true);
|
||||
}
|
||||
|
||||
goldens ??= SkiaGoldClient(baseDirectory);
|
||||
await goldens.auth();
|
||||
await goldens.auth(baseDirectory);
|
||||
await goldens.imgtestInit();
|
||||
return FlutterSkiaGoldFileComparator(baseDirectory.uri, goldens);
|
||||
}
|
||||
|
@ -219,14 +219,20 @@ class FlutterSkiaGoldFileComparator extends FlutterGoldenFileComparator {
|
|||
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
|
||||
golden = _addPrefix(golden);
|
||||
await update(golden, imageBytes);
|
||||
final File goldenFile = getGoldenFile(golden);
|
||||
|
||||
return skiaClient.imgtestAdd(golden.path, goldenFile);
|
||||
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 isAvailableForEnvironment(Platform platform) {
|
||||
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'] ?? '';
|
||||
|
@ -236,210 +242,49 @@ class FlutterSkiaGoldFileComparator extends FlutterGoldenFileComparator {
|
|||
&& 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 testing golden images before changes are
|
||||
/// merged into the master branch.
|
||||
///
|
||||
/// This comparator utilizes the [SkiaGoldClient] to request baseline images for
|
||||
/// the given device under test for comparison. This comparator is only
|
||||
/// initialized during pre-submit testing on Cirrus CI.
|
||||
/// 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:
|
||||
///
|
||||
/// * [GoldenFileComparator], the abstract class that
|
||||
/// [FlutterGoldenFileComparator] implements.
|
||||
/// * [FlutterSkiaGoldFileComparator], another
|
||||
/// [FlutterGoldenFileComparator] that uploads tests to the Skia Gold
|
||||
/// dashboard.
|
||||
/// * [FlutterLocalFileComparator], another
|
||||
/// [FlutterGoldenFileComparator] that tests golden images locally on your
|
||||
/// current machine.
|
||||
class FlutterPreSubmitFileComparator extends FlutterGoldenFileComparator {
|
||||
/// Creates a [FlutterPreSubmitFileComparator] that will test golden file
|
||||
/// images against baselines requested from Flutter Gold.
|
||||
///
|
||||
/// The [fs] and [platform] parameters are useful in tests, where the default
|
||||
/// file system and platform can be replaced by mock instances.
|
||||
FlutterPreSubmitFileComparator(
|
||||
final Uri basedir,
|
||||
final SkiaGoldClient skiaClient, {
|
||||
final FileSystem fs = const LocalFileSystem(),
|
||||
final Platform platform = const LocalPlatform(),
|
||||
}) : super(
|
||||
basedir,
|
||||
skiaClient,
|
||||
fs: fs,
|
||||
platform: platform,
|
||||
);
|
||||
/// * [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 [FlutterPreSubmitFileComparator] that mirrors the
|
||||
/// relative path resolution of the default [goldenFileComparator].
|
||||
///
|
||||
/// The [goldens] and [defaultComparator] parameters are visible for testing
|
||||
/// purposes only.
|
||||
static Future<FlutterGoldenFileComparator> fromDefaultComparator(
|
||||
final Platform platform, {
|
||||
SkiaGoldClient goldens,
|
||||
/// Creates a new [FlutterSkippingGoldenFileComparator] that mirrors the relative
|
||||
/// path resolution of the default [goldenFileComparator].
|
||||
static FlutterSkippingGoldenFileComparator fromDefaultComparator({
|
||||
LocalFileComparator defaultComparator,
|
||||
}) async {
|
||||
|
||||
}) {
|
||||
defaultComparator ??= goldenFileComparator;
|
||||
final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(
|
||||
defaultComparator,
|
||||
platform,
|
||||
);
|
||||
|
||||
if(!baseDirectory.existsSync()) {
|
||||
baseDirectory.createSync(recursive: true);
|
||||
}
|
||||
|
||||
goldens ??= SkiaGoldClient(baseDirectory);
|
||||
await goldens.getExpectations();
|
||||
|
||||
return FlutterPreSubmitFileComparator(baseDirectory.uri, goldens);
|
||||
return FlutterSkippingGoldenFileComparator(defaultComparator.basedir);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
|
||||
golden = _addPrefix(golden);
|
||||
final String testName = skiaClient.cleanTestName(golden.path);
|
||||
final List<String> testExpectations = skiaClient.expectations[testName];
|
||||
|
||||
if (testExpectations == null) {
|
||||
// There is no baseline for this test
|
||||
return true;
|
||||
}
|
||||
|
||||
ComparisonResult result;
|
||||
for (String expectation in testExpectations) {
|
||||
final List<int> goldenBytes = await skiaClient.getImageBytes(expectation);
|
||||
|
||||
result = GoldenFileComparator.compareLists(
|
||||
imageBytes,
|
||||
goldenBytes,
|
||||
);
|
||||
|
||||
if (result.passed) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return skiaClient.testIsIgnoredForPullRequest(
|
||||
platform.environment['CIRRUS_PR'] ?? '',
|
||||
golden.path,
|
||||
print('Skipping "$golden" test : Skia Gold is not available in this testing '
|
||||
'environment and flutter/goldens repository comparison is only available '
|
||||
'on Linux machines.'
|
||||
);
|
||||
}
|
||||
|
||||
/// Decides based on the current environment whether goldens tests should be
|
||||
/// performed as pre-submit tests with Skia Gold.
|
||||
static bool isAvailableForEnvironment(Platform platform) {
|
||||
final String cirrusCI = platform.environment['CIRRUS_CI'] ?? '';
|
||||
final String cirrusPR = platform.environment['CIRRUS_PR'] ?? '';
|
||||
return cirrusCI.isNotEmpty && cirrusPR.isNotEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
/// A [FlutterGoldenFileComparator] for testing golden images locally on your
|
||||
/// current machine.
|
||||
///
|
||||
/// This comparator utilizes the [SkiaGoldClient] to request baseline images for
|
||||
/// the given device under test for comparison. This comparator is only
|
||||
/// initialized when running tests locally, and is intended to serve as a smoke
|
||||
/// test during development. As such, it will not be able to detect unintended
|
||||
/// changes on other machines until it they are tested using the
|
||||
/// [FlutterPreSubmitFileComparator].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [GoldenFileComparator], the abstract class that
|
||||
/// [FlutterGoldenFileComparator] implements.
|
||||
/// * [FlutterSkiaGoldFileComparator], another
|
||||
/// [FlutterGoldenFileComparator] that uploads tests to the Skia Gold
|
||||
/// dashboard.
|
||||
/// * [FlutterPreSubmitFileComparator], another
|
||||
/// [FlutterGoldenFileComparator] that tests golden images before changes are
|
||||
/// merged into the master branch.
|
||||
class FlutterLocalFileComparator extends FlutterGoldenFileComparator with LocalComparisonOutput {
|
||||
/// Creates a [FlutterLocalFileComparator] that will test golden file
|
||||
/// images against baselines requested from Flutter Gold.
|
||||
///
|
||||
/// The [fs] and [platform] parameters are useful in tests, where the default
|
||||
/// file system and platform can be replaced by mock instances.
|
||||
FlutterLocalFileComparator(
|
||||
final Uri basedir,
|
||||
final SkiaGoldClient skiaClient, {
|
||||
final FileSystem fs = const LocalFileSystem(),
|
||||
final Platform platform = const LocalPlatform(),
|
||||
}) : super(
|
||||
basedir,
|
||||
skiaClient,
|
||||
fs: fs,
|
||||
platform: platform,
|
||||
);
|
||||
|
||||
/// Creates a new [FlutterLocalFileComparator] that mirrors the
|
||||
/// relative path resolution of the default [goldenFileComparator].
|
||||
///
|
||||
/// The [goldens] and [defaultComparator] parameters are visible for testing
|
||||
/// purposes only.
|
||||
static Future<FlutterGoldenFileComparator> fromDefaultComparator(
|
||||
final Platform platform, {
|
||||
SkiaGoldClient goldens,
|
||||
LocalFileComparator defaultComparator,
|
||||
}) async {
|
||||
|
||||
defaultComparator ??= goldenFileComparator;
|
||||
final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(
|
||||
defaultComparator,
|
||||
platform,
|
||||
);
|
||||
|
||||
if(!baseDirectory.existsSync()) {
|
||||
baseDirectory.createSync(recursive: true);
|
||||
}
|
||||
|
||||
goldens ??= SkiaGoldClient(baseDirectory);
|
||||
await goldens.getExpectations();
|
||||
|
||||
return FlutterLocalFileComparator(baseDirectory.uri, goldens);
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
|
||||
golden = _addPrefix(golden);
|
||||
final String testName = skiaClient.cleanTestName(golden.path);
|
||||
final List<String> testExpectations = skiaClient.expectations[testName];
|
||||
if (testExpectations == null) {
|
||||
// There is no baseline for this test
|
||||
print('No expectations provided by Skia Gold for test: $golden. '
|
||||
'This may be a new test. If this is an unexpected result, check'
|
||||
' https://flutter-gold.skia.org.\n'
|
||||
'Validate image output found at $basedir'
|
||||
);
|
||||
update(golden, imageBytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
ComparisonResult result;
|
||||
final Map<String, ComparisonResult> validFailures = <String, ComparisonResult>{};
|
||||
for (String expectation in testExpectations) {
|
||||
final List<int> goldenBytes = await skiaClient.getImageBytes(expectation);
|
||||
|
||||
result = GoldenFileComparator.compareLists(
|
||||
imageBytes,
|
||||
goldenBytes,
|
||||
);
|
||||
|
||||
if (result.passed) {
|
||||
return true;
|
||||
} else if (await skiaClient.isValidDigestForExpectation(expectation, golden.path)) {
|
||||
validFailures[expectation] = result;
|
||||
}
|
||||
}
|
||||
validFailures.forEach((String expectation, ComparisonResult result) {
|
||||
generateFailureOutput(result, golden, basedir, key: expectation);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
Future<void> update(Uri golden, Uint8List imageBytes) => null;
|
||||
}
|
||||
|
|
|
@ -2,10 +2,7 @@
|
|||
// 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:core';
|
||||
import 'dart:io';
|
||||
import 'dart:io' as io;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
|
@ -16,544 +13,196 @@ import 'package:mockito/mockito.dart';
|
|||
import 'package:platform/platform.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import 'json_templates.dart';
|
||||
|
||||
const String _kFlutterRoot = '/flutter';
|
||||
|
||||
// 1x1 transparent pixel
|
||||
const List<int> _kTestPngBytes =
|
||||
<int>[137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0,
|
||||
1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84,
|
||||
120, 1, 99, 97, 0, 2, 0, 0, 25, 0, 5, 144, 240, 54, 245, 0, 0, 0, 0, 73, 69,
|
||||
78, 68, 174, 66, 96, 130];
|
||||
|
||||
// 1x1 colored pixel
|
||||
const List<int> _kFailPngBytes =
|
||||
<int>[137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0,
|
||||
1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84,
|
||||
120, 1, 99, 249, 207, 240, 255, 63, 0, 7, 18, 3, 2, 164, 147, 160, 197, 0,
|
||||
0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130];
|
||||
const String _kRepositoryRoot = '$_kFlutterRoot/bin/cache/pkg/goldens';
|
||||
const String _kVersionFile = '$_kFlutterRoot/bin/internal/goldens.version';
|
||||
const String _kGoldensVersion = '123456abcdef';
|
||||
|
||||
void main() {
|
||||
MemoryFileSystem fs;
|
||||
FakePlatform platform;
|
||||
MockProcessManager process;
|
||||
MockHttpClient mockHttpClient;
|
||||
|
||||
setUp(() {
|
||||
fs = MemoryFileSystem();
|
||||
platform = FakePlatform(
|
||||
environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot},
|
||||
operatingSystem: 'macos'
|
||||
);
|
||||
platform = FakePlatform(environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot});
|
||||
process = MockProcessManager();
|
||||
mockHttpClient = MockHttpClient();
|
||||
fs.directory(_kFlutterRoot).createSync(recursive: true);
|
||||
fs.directory(_kRepositoryRoot).createSync(recursive: true);
|
||||
fs.file(_kVersionFile).createSync(recursive: true);
|
||||
fs.file(_kVersionFile).writeAsStringSync(_kGoldensVersion);
|
||||
});
|
||||
|
||||
group('SkiaGoldClient', () {
|
||||
SkiaGoldClient skiaClient;
|
||||
group('GoldensClient', () {
|
||||
GoldensRepositoryClient goldens;
|
||||
|
||||
setUp(() {
|
||||
final Directory workDirectory = fs.directory('/workDirectory')
|
||||
..createSync(recursive: true);
|
||||
skiaClient = SkiaGoldClient(
|
||||
workDirectory,
|
||||
goldens = GoldensRepositoryClient(
|
||||
fs: fs,
|
||||
process: process,
|
||||
platform: platform,
|
||||
);
|
||||
});
|
||||
|
||||
group('prepare', () {
|
||||
test('performs minimal work if versions match', () async {
|
||||
when(process.run(any, workingDirectory: anyNamed('workingDirectory')))
|
||||
.thenAnswer((_) => Future<io.ProcessResult>.value(io.ProcessResult(123, 0, _kGoldensVersion, '')));
|
||||
await goldens.prepare();
|
||||
|
||||
// Verify that we only spawned `git rev-parse HEAD`
|
||||
final VerificationResult verifyProcessRun =
|
||||
verify(process.run(captureAny, workingDirectory: captureAnyNamed('workingDirectory')));
|
||||
verifyProcessRun.called(1);
|
||||
expect(verifyProcessRun.captured.first, <String>['git', 'rev-parse', 'HEAD']);
|
||||
expect(verifyProcessRun.captured.last, _kRepositoryRoot);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('SkiaGoldClient', () {
|
||||
SkiaGoldClient goldens;
|
||||
|
||||
setUp(() {
|
||||
goldens = SkiaGoldClient(
|
||||
fs: fs,
|
||||
process: process,
|
||||
platform: platform,
|
||||
httpClient: mockHttpClient,
|
||||
);
|
||||
});
|
||||
|
||||
group('auth', () {
|
||||
test('performs minimal work if already authorized', () async {
|
||||
fs.file('/workDirectory/temp/auth_opt.json')
|
||||
..createSync(recursive: true);
|
||||
when(process.run(any))
|
||||
.thenAnswer((_) => Future<ProcessResult>
|
||||
.value(ProcessResult(123, 0, '', '')));
|
||||
await skiaClient.auth();
|
||||
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);
|
||||
|
||||
verifyNever(process.run(
|
||||
captureAny,
|
||||
workingDirectory: captureAnyNamed('workingDirectory'),
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
group('Request Handling', () {
|
||||
String testName;
|
||||
String pullRequestNumber;
|
||||
String expectation;
|
||||
Uri url;
|
||||
MockHttpClientRequest mockHttpRequest;
|
||||
|
||||
setUp(() {
|
||||
testName = 'flutter.golden_test.1.png';
|
||||
pullRequestNumber = '1234';
|
||||
expectation = '55109a4bed52acc780530f7a9aeff6c0';
|
||||
mockHttpRequest = MockHttpClientRequest();
|
||||
});
|
||||
|
||||
test('validates SkiaDigest', () {
|
||||
final Map<String, dynamic> skiaJson = json.decode(digestResponseTemplate());
|
||||
final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
|
||||
expect(
|
||||
digest.isValid(
|
||||
platform,
|
||||
'flutter.golden_test.1',
|
||||
expectation,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('invalidates bad SkiaDigest - platform', () {
|
||||
final Map<String, dynamic> skiaJson = json.decode(
|
||||
digestResponseTemplate(platform: 'linux')
|
||||
);
|
||||
final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
|
||||
expect(
|
||||
digest.isValid(
|
||||
platform,
|
||||
'flutter.golden_test.1',
|
||||
expectation,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('invalidates bad SkiaDigest - test name', () {
|
||||
final Map<String, dynamic> skiaJson = json.decode(
|
||||
digestResponseTemplate(testName: 'flutter.golden_test.2')
|
||||
);
|
||||
final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
|
||||
expect(
|
||||
digest.isValid(
|
||||
platform,
|
||||
'flutter.golden_test.1',
|
||||
expectation,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('invalidates bad SkiaDigest - expectation', () {
|
||||
final Map<String, dynamic> skiaJson = json.decode(
|
||||
digestResponseTemplate(expectation: '1deg543sf645erg44awqcc78')
|
||||
);
|
||||
final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
|
||||
expect(
|
||||
digest.isValid(
|
||||
platform,
|
||||
'flutter.golden_test.1',
|
||||
expectation,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('invalidates bad SkiaDigest - status', () {
|
||||
final Map<String, dynamic> skiaJson = json.decode(
|
||||
digestResponseTemplate(status: 'negative')
|
||||
);
|
||||
final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
|
||||
expect(
|
||||
digest.isValid(
|
||||
platform,
|
||||
'flutter.golden_test.1',
|
||||
expectation,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('sets up expectations', () async {
|
||||
url = Uri.parse('https://flutter-gold.skia.org/json/expectations/commit/HEAD');
|
||||
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
|
||||
utf8.encode(rawExpectationsTemplate())
|
||||
);
|
||||
when(mockHttpClient.getUrl(url))
|
||||
.thenAnswer((_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
|
||||
when(mockHttpRequest.close())
|
||||
.thenAnswer((_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
|
||||
|
||||
await skiaClient.getExpectations();
|
||||
expect(skiaClient.expectations, isNotNull);
|
||||
expect(
|
||||
skiaClient.expectations['flutter.golden_test.1'],
|
||||
contains(expectation),
|
||||
);
|
||||
});
|
||||
|
||||
test('detects invalid digests SkiaDigest', () {
|
||||
const String testName = 'flutter.golden_test.2';
|
||||
final Map<String, dynamic> skiaJson = json.decode(digestResponseTemplate());
|
||||
final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
|
||||
expect(digest.isValid(platform, testName, expectation), isFalse);
|
||||
});
|
||||
|
||||
test('image bytes are processed properly', () async {
|
||||
final Uri imageUrl = Uri.parse(
|
||||
'https://flutter-gold.skia.org/img/images/$expectation.png'
|
||||
);
|
||||
final MockHttpClientRequest mockImageRequest = MockHttpClientRequest();
|
||||
final MockHttpImageResponse mockImageResponse = MockHttpImageResponse(
|
||||
imageResponseTemplate()
|
||||
);
|
||||
when(mockHttpClient.getUrl(imageUrl))
|
||||
.thenAnswer((_) => Future<MockHttpClientRequest>.value(mockImageRequest));
|
||||
when(mockImageRequest.close())
|
||||
.thenAnswer((_) => Future<MockHttpImageResponse>.value(mockImageResponse));
|
||||
|
||||
final List<int> masterBytes = await skiaClient.getImageBytes(expectation);
|
||||
|
||||
expect(masterBytes, equals(_kTestPngBytes));
|
||||
});
|
||||
|
||||
group('ignores', () {
|
||||
Uri url;
|
||||
MockHttpClientRequest mockHttpRequest;
|
||||
MockHttpClientResponse mockHttpResponse;
|
||||
|
||||
setUp(() {
|
||||
url = Uri.parse('https://flutter-gold.skia.org/json/ignores');
|
||||
mockHttpRequest = MockHttpClientRequest();
|
||||
mockHttpResponse = MockHttpClientResponse(utf8.encode(
|
||||
ignoreResponseTemplate(pullRequestNumber: pullRequestNumber)
|
||||
));
|
||||
when(mockHttpClient.getUrl(url))
|
||||
.thenAnswer((_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
|
||||
when(mockHttpRequest.close())
|
||||
.thenAnswer((_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
|
||||
});
|
||||
|
||||
test('returns true for ignored test and ignored pull request number', () async {
|
||||
expect(
|
||||
await skiaClient.testIsIgnoredForPullRequest(
|
||||
pullRequestNumber,
|
||||
testName,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns false for not ignored test and ignored pull request number', () async {
|
||||
expect(
|
||||
await skiaClient.testIsIgnoredForPullRequest(
|
||||
'5678',
|
||||
testName,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns false for ignored test and not ignored pull request number', () async {
|
||||
expect(
|
||||
await skiaClient.testIsIgnoredForPullRequest(
|
||||
pullRequestNumber,
|
||||
'failure.png',
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('digest parsing', () {
|
||||
Uri url;
|
||||
MockHttpClientRequest mockHttpRequest;
|
||||
MockHttpClientResponse mockHttpResponse;
|
||||
|
||||
setUp(() {
|
||||
url = Uri.parse(
|
||||
'https://flutter-gold.skia.org/json/details?'
|
||||
'test=flutter.golden_test.1&digest=$expectation'
|
||||
);
|
||||
mockHttpRequest = MockHttpClientRequest();
|
||||
when(mockHttpClient.getUrl(url))
|
||||
.thenAnswer((_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
|
||||
});
|
||||
|
||||
test('succeeds when valid', () async {
|
||||
mockHttpResponse = MockHttpClientResponse(utf8.encode(digestResponseTemplate()));
|
||||
when(mockHttpRequest.close())
|
||||
.thenAnswer((_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
|
||||
expect(
|
||||
await skiaClient.isValidDigestForExpectation(
|
||||
expectation,
|
||||
testName,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails when invalid', () async {
|
||||
mockHttpResponse = MockHttpClientResponse(utf8.encode(
|
||||
digestResponseTemplate(platform: 'linux')
|
||||
));
|
||||
when(mockHttpRequest.close())
|
||||
.thenAnswer((_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
|
||||
expect(
|
||||
await skiaClient.isValidDigestForExpectation(
|
||||
expectation,
|
||||
testName,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
// 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;
|
||||
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 = FlutterGoldensRepositoryFileComparator(
|
||||
testDirectory.uri,
|
||||
fs: fs,
|
||||
platform: platform,
|
||||
);
|
||||
});
|
||||
|
||||
group('compare', () {
|
||||
test('throws if golden file is not found', () async {
|
||||
try {
|
||||
await comparator.compare(Uint8List.fromList(<int>[1, 2, 3]), Uri.parse('test.png'));
|
||||
fail('TestFailure expected but not thrown');
|
||||
} on TestFailure catch (error) {
|
||||
expect(error.message, contains('Could not be compared against non-existent file'));
|
||||
}
|
||||
});
|
||||
|
||||
test('returns false if golden bytes do not match', () async {
|
||||
final File goldenFile = fs.file('/path/to/flutter/bin/cache/goldens/test/foo/bar/test.png')
|
||||
..createSync(recursive: true);
|
||||
goldenFile.writeAsBytesSync(<int>[4, 5, 6], flush: true);
|
||||
final bool result = await comparator.compare(Uint8List.fromList(<int>[1, 2, 3]), Uri.parse('test.png'));
|
||||
expect(result, isFalse);
|
||||
});
|
||||
|
||||
test('returns true if golden bytes match', () async {
|
||||
final File goldenFile = fs.file('/path/to/flutter/bin/cache/goldens/test/foo/bar/test.png')
|
||||
..createSync(recursive: true);
|
||||
goldenFile.writeAsBytesSync(<int>[1, 2, 3], flush: true);
|
||||
final bool result = await comparator.compare(Uint8List.fromList(<int>[1, 2, 3]), Uri.parse('test.png'));
|
||||
expect(result, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('update', () {
|
||||
test('creates golden file if it does not already exist', () async {
|
||||
final File goldenFile = fs.file('/path/to/flutter/bin/cache/goldens/test/foo/bar/test.png');
|
||||
expect(goldenFile.existsSync(), isFalse);
|
||||
await comparator.update(Uri.parse('test.png'), Uint8List.fromList(<int>[1, 2, 3]));
|
||||
expect(goldenFile.existsSync(), isTrue);
|
||||
expect(goldenFile.readAsBytesSync(), <int>[1, 2, 3]);
|
||||
});
|
||||
|
||||
test('overwrites golden bytes if golden file already exist', () async {
|
||||
final File goldenFile = fs.file('/path/to/flutter/bin/cache/goldens/test/foo/bar/test.png')
|
||||
..createSync(recursive: true);
|
||||
goldenFile.writeAsBytesSync(<int>[4, 5, 6], flush: true);
|
||||
await comparator.update(Uri.parse('test.png'), Uint8List.fromList(<int>[1, 2, 3]));
|
||||
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 basedir = fs.directory('flutter/test/library/')
|
||||
..createSync(recursive: true);
|
||||
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(
|
||||
basedir.uri,
|
||||
testDirectory.uri,
|
||||
MockSkiaGoldClient(),
|
||||
fs: fs,
|
||||
platform: platform,
|
||||
);
|
||||
});
|
||||
|
||||
test('calculates the basedir correctly from defaultComparator', () async {
|
||||
final MockLocalFileComparator defaultComparator = MockLocalFileComparator();
|
||||
final Directory flutterRoot = fs.directory(platform.environment['FLUTTER_ROOT'])
|
||||
..createSync(recursive: true);
|
||||
when(defaultComparator.basedir).thenReturn(flutterRoot.childDirectory('baz').uri);
|
||||
|
||||
final Directory basedir = FlutterGoldenFileComparator.getBaseDirectory(
|
||||
defaultComparator,
|
||||
platform,
|
||||
);
|
||||
expect(
|
||||
basedir.uri,
|
||||
fs.directory('/flutter/bin/cache/pkg/skia_goldens/baz').uri,
|
||||
);
|
||||
});
|
||||
|
||||
test('ignores version number', () {
|
||||
final Uri key = comparator.getTestUri(Uri.parse('foo.png'), 1);
|
||||
expect(key, Uri.parse('foo.png'));
|
||||
});
|
||||
|
||||
group('Post-Submit', () {
|
||||
final MockSkiaGoldClient mockSkiaClient = MockSkiaGoldClient();
|
||||
|
||||
setUp(() {
|
||||
final Directory basedir = fs.directory('flutter/test/library/')
|
||||
..createSync(recursive: true);
|
||||
comparator = FlutterSkiaGoldFileComparator(
|
||||
basedir.uri,
|
||||
mockSkiaClient,
|
||||
fs: fs,
|
||||
platform: platform,
|
||||
);
|
||||
});
|
||||
|
||||
test('correctly determines testing environment', () {
|
||||
platform = FakePlatform(
|
||||
environment: <String, String>{
|
||||
'FLUTTER_ROOT': _kFlutterRoot,
|
||||
'CIRRUS_CI' : 'true',
|
||||
'CIRRUS_PR' : '',
|
||||
'CIRRUS_BRANCH' : 'master',
|
||||
'GOLD_SERVICE_ACCOUNT' : 'service account...',
|
||||
},
|
||||
operatingSystem: 'macos'
|
||||
);
|
||||
expect(
|
||||
FlutterSkiaGoldFileComparator.isAvailableForEnvironment(platform),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Pre-Submit', () {
|
||||
FlutterPreSubmitFileComparator comparator;
|
||||
final MockSkiaGoldClient mockSkiaClient = MockSkiaGoldClient();
|
||||
|
||||
setUp(() {
|
||||
final Directory basedir = fs.directory('flutter/test/library/')
|
||||
..createSync(recursive: true);
|
||||
comparator = FlutterPreSubmitFileComparator(
|
||||
basedir.uri,
|
||||
mockSkiaClient,
|
||||
fs: fs,
|
||||
platform: FakePlatform(
|
||||
environment: <String, String>{
|
||||
'FLUTTER_ROOT': _kFlutterRoot,
|
||||
'CIRRUS_CI' : 'true',
|
||||
'CIRRUS_PR' : '1234',
|
||||
},
|
||||
operatingSystem: 'macos'
|
||||
),
|
||||
);
|
||||
|
||||
when(mockSkiaClient.getImageBytes('55109a4bed52acc780530f7a9aeff6c0'))
|
||||
.thenAnswer((_) => Future<List<int>>.value(_kTestPngBytes));
|
||||
when(mockSkiaClient.expectations)
|
||||
.thenReturn(expectationsTemplate());
|
||||
when(mockSkiaClient.cleanTestName('library.flutter.golden_test.1.png'))
|
||||
.thenReturn('flutter.golden_test.1');
|
||||
when(mockSkiaClient.isValidDigestForExpectation(
|
||||
'55109a4bed52acc780530f7a9aeff6c0',
|
||||
'library.flutter.golden_test.1.png',
|
||||
))
|
||||
.thenAnswer((_) => Future<bool>.value(false));
|
||||
});
|
||||
|
||||
test('correctly determines testing environment', () {
|
||||
platform = FakePlatform(
|
||||
environment: <String, String>{
|
||||
'FLUTTER_ROOT': _kFlutterRoot,
|
||||
'CIRRUS_CI' : 'true',
|
||||
'CIRRUS_PR' : '1234',
|
||||
},
|
||||
operatingSystem: 'macos'
|
||||
);
|
||||
expect(
|
||||
FlutterPreSubmitFileComparator.isAvailableForEnvironment(platform),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('comparison passes test that is ignored for this PR', () async {
|
||||
when(mockSkiaClient.getImageBytes('55109a4bed52acc780530f7a9aeff6c0'))
|
||||
.thenAnswer((_) => Future<List<int>>.value(_kTestPngBytes));
|
||||
when(mockSkiaClient.testIsIgnoredForPullRequest(
|
||||
'1234',
|
||||
'library.flutter.golden_test.1.png',
|
||||
))
|
||||
.thenAnswer((_) => Future<bool>.value(true));
|
||||
expect(
|
||||
await comparator.compare(
|
||||
Uint8List.fromList(_kFailPngBytes),
|
||||
Uri.parse('flutter.golden_test.1.png'),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails test that is not ignored for this PR', () async {
|
||||
when(mockSkiaClient.getImageBytes('55109a4bed52acc780530f7a9aeff6c0'))
|
||||
.thenAnswer((_) => Future<List<int>>.value(_kTestPngBytes));
|
||||
when(mockSkiaClient.testIsIgnoredForPullRequest(
|
||||
'1234',
|
||||
'library.flutter.golden_test.1.png',
|
||||
))
|
||||
.thenAnswer((_) => Future<bool>.value(false));
|
||||
expect(
|
||||
await comparator.compare(
|
||||
Uint8List.fromList(_kFailPngBytes),
|
||||
Uri.parse('flutter.golden_test.1.png'),
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('passes non-existent baseline for new test', () async {
|
||||
expect(
|
||||
await comparator.compare(
|
||||
Uint8List.fromList(_kFailPngBytes),
|
||||
Uri.parse('flutter.new_golden_test.1.png'),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Local', () {
|
||||
FlutterLocalFileComparator comparator;
|
||||
final MockSkiaGoldClient mockSkiaClient = MockSkiaGoldClient();
|
||||
|
||||
setUp(() async {
|
||||
final Directory basedir = fs.directory('flutter/test/library/')
|
||||
..createSync(recursive: true);
|
||||
comparator = FlutterLocalFileComparator(
|
||||
basedir.uri,
|
||||
mockSkiaClient,
|
||||
fs: fs,
|
||||
platform: FakePlatform(
|
||||
environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot},
|
||||
operatingSystem: 'macos'
|
||||
),
|
||||
);
|
||||
|
||||
when(mockSkiaClient.getImageBytes('55109a4bed52acc780530f7a9aeff6c0'))
|
||||
.thenAnswer((_) => Future<List<int>>.value(_kTestPngBytes));
|
||||
when(mockSkiaClient.expectations)
|
||||
.thenReturn(expectationsTemplate());
|
||||
when(mockSkiaClient.cleanTestName('library.flutter.golden_test.1.png'))
|
||||
.thenReturn('flutter.golden_test.1');
|
||||
when(mockSkiaClient.isValidDigestForExpectation(
|
||||
'55109a4bed52acc780530f7a9aeff6c0',
|
||||
'library.flutter.golden_test.1.png',
|
||||
))
|
||||
.thenAnswer((_) => Future<bool>.value(false));
|
||||
});
|
||||
|
||||
test('passes when bytes match', () async {
|
||||
expect(
|
||||
await comparator.compare(
|
||||
Uint8List.fromList(_kTestPngBytes),
|
||||
Uri.parse('flutter.golden_test.1.png'),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('passes non-existent baseline for new test', () async {
|
||||
expect(
|
||||
await comparator.compare(
|
||||
Uint8List.fromList(_kFailPngBytes),
|
||||
Uri.parse('flutter.new_golden_test.1'),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
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 MockGoldensRepositoryClient extends Mock implements GoldensRepositoryClient {}
|
||||
class MockSkiaGoldClient extends Mock implements SkiaGoldClient {}
|
||||
|
||||
class MockLocalFileComparator extends Mock implements LocalFileComparator {}
|
||||
|
||||
class MockHttpClient extends Mock implements HttpClient {}
|
||||
|
||||
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
|
||||
|
||||
class MockHttpClientResponse extends Mock implements HttpClientResponse {
|
||||
MockHttpClientResponse(this.response);
|
||||
|
||||
final Uint8List response;
|
||||
|
||||
@override
|
||||
StreamSubscription<Uint8List> listen(
|
||||
void onData(Uint8List event), {
|
||||
Function onError,
|
||||
void onDone(),
|
||||
bool cancelOnError,
|
||||
}) {
|
||||
return Stream<Uint8List>.fromFuture(Future<Uint8List>.value(response))
|
||||
.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
|
||||
}
|
||||
}
|
||||
|
||||
class MockHttpImageResponse extends Mock implements HttpClientResponse {
|
||||
MockHttpImageResponse(this.response);
|
||||
|
||||
final List<List<int>> response;
|
||||
|
||||
@override
|
||||
Future<void> forEach(void action(List<int> element)) async {
|
||||
response.forEach(action);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,202 +0,0 @@
|
|||
// 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.
|
||||
|
||||
/// Json response template for Skia Gold expectations request:
|
||||
/// https://flutter-gold.skia.org/json/expectations/commit/HEAD
|
||||
String rawExpectationsTemplate() {
|
||||
return '''
|
||||
{
|
||||
"md5": "a7489b00e03a1846e43500b7c14dd7b0",
|
||||
"master": {
|
||||
"flutter.golden_test.1": {
|
||||
"55109a4bed52acc780530f7a9aeff6c0": 1
|
||||
},
|
||||
"flutter.golden_test.3": {
|
||||
"87cb35131e6ad4b57d4d09d59ae743c3": 1,
|
||||
"dc94eb2c39c0c8ae11a4efd090b72f94": 1,
|
||||
"f2583c9003978a06b7888878bdc089e2": 1
|
||||
},
|
||||
"flutter.golden_test.2": {
|
||||
"eb03a5e3114c9ecad5e4f1178f285a49": 1,
|
||||
"f14631979de24fca6e14ad247d5f2bd6": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
}
|
||||
|
||||
/// Decoded json response template for Skia Gold expectations request:
|
||||
/// https://flutter-gold.skia.org/json/expectations/commit/HEAD
|
||||
Map<String, List<String>> expectationsTemplate() {
|
||||
return <String, List<String>>{
|
||||
'flutter.golden_test.1': <String>[
|
||||
'55109a4bed52acc780530f7a9aeff6c0'
|
||||
],
|
||||
'flutter.golden_test.3': <String>[
|
||||
'87cb35131e6ad4b57d4d09d59ae743c3',
|
||||
'dc94eb2c39c0c8ae11a4efd090b72f94',
|
||||
'f2583c9003978a06b7888878bdc089e2',
|
||||
],
|
||||
'flutter.golden_test.2': <String>[
|
||||
'eb03a5e3114c9ecad5e4f1178f285a49',
|
||||
'f14631979de24fca6e14ad247d5f2bd6',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/// Json response template for Skia Gold digest request:
|
||||
/// https://flutter-gold.skia.org/json/details?test=[testName]&digest=[expectation]
|
||||
String digestResponseTemplate({
|
||||
String testName = 'flutter.golden_test.1',
|
||||
String expectation = '55109a4bed52acc780530f7a9aeff6c0',
|
||||
String platform = 'macos',
|
||||
String status = 'positive',
|
||||
}) {
|
||||
return '''
|
||||
{
|
||||
"digest": {
|
||||
"test": "$testName",
|
||||
"digest": "$expectation",
|
||||
"status": "$status",
|
||||
"paramset": {
|
||||
"Platform": [
|
||||
"$platform"
|
||||
],
|
||||
"ext": [
|
||||
"png"
|
||||
],
|
||||
"name": [
|
||||
"$testName"
|
||||
],
|
||||
"source_type": [
|
||||
"flutter"
|
||||
]
|
||||
},
|
||||
"traces": {
|
||||
"tileSize": 200,
|
||||
"traces": [
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"s": 0
|
||||
},
|
||||
{
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"s": 0
|
||||
},
|
||||
{
|
||||
"x": 199,
|
||||
"y": 0,
|
||||
"s": 0
|
||||
}
|
||||
],
|
||||
"label": ",Platform=$platform,name=$testName,source_type=flutter,",
|
||||
"params": {
|
||||
"Platform": "$platform",
|
||||
"ext": "png",
|
||||
"name": "$testName",
|
||||
"source_type": "flutter"
|
||||
}
|
||||
}
|
||||
],
|
||||
"digests": [
|
||||
{
|
||||
"digest": "$expectation",
|
||||
"status": "$status"
|
||||
}
|
||||
]
|
||||
},
|
||||
"closestRef": "pos",
|
||||
"refDiffs": {
|
||||
"neg": null,
|
||||
"pos": {
|
||||
"numDiffPixels": 999,
|
||||
"pixelDiffPercent": 0.4995,
|
||||
"maxRGBADiffs": [
|
||||
86,
|
||||
86,
|
||||
86,
|
||||
0
|
||||
],
|
||||
"dimDiffer": false,
|
||||
"diffs": {
|
||||
"combined": 0.381955,
|
||||
"percent": 0.4995,
|
||||
"pixel": 999
|
||||
},
|
||||
"digest": "aa748136c70cefdda646df5be0ae189d",
|
||||
"status": "positive",
|
||||
"paramset": {
|
||||
"Platform": [
|
||||
"macos"
|
||||
],
|
||||
"ext": [
|
||||
"png"
|
||||
],
|
||||
"name": [
|
||||
"$testName"
|
||||
],
|
||||
"source_type": [
|
||||
"flutter"
|
||||
]
|
||||
},
|
||||
"n": 197
|
||||
}
|
||||
}
|
||||
},
|
||||
"commits": [
|
||||
{
|
||||
"commit_time": 1568069344,
|
||||
"hash": "399bb04e2de41665320d3c888f40af6d8bc734a2",
|
||||
"author": "Contributor A (contributorA@getMail.com)"
|
||||
},
|
||||
{
|
||||
"commit_time": 1568078053,
|
||||
"hash": "0f365d3add253a65e5e5af1024f56c6169bf9739",
|
||||
"author": "Contributor B (contributorB@getMail.com)"
|
||||
},
|
||||
{
|
||||
"commit_time": 1569353925,
|
||||
"hash": "81e693a7fe3b808cc9ae2bb3a2cbe404e67ec773",
|
||||
"author": "Contributor C (contributorC@getMail.com)"
|
||||
}
|
||||
]
|
||||
}
|
||||
''';
|
||||
}
|
||||
|
||||
/// Json response template for Skia Gold ignore request:
|
||||
/// https://flutter-gold.skia.org/json/ignores
|
||||
String ignoreResponseTemplate({
|
||||
String pullRequestNumber = '0000',
|
||||
String testName = 'flutter.golden_test.1',
|
||||
}) {
|
||||
return '''
|
||||
[
|
||||
{
|
||||
"id": "7579425228619212078",
|
||||
"name": "contributor@getMail.com",
|
||||
"updatedBy": "contributor@getMail.com",
|
||||
"expires": "2019-09-06T21:28:18.815336Z",
|
||||
"query": "ext=png&name=$testName",
|
||||
"note": "https://github.com/flutter/flutter/pull/$pullRequestNumber"
|
||||
}
|
||||
]
|
||||
''';
|
||||
}
|
||||
|
||||
/// Json response template for Skia Gold image request:
|
||||
/// https://flutter-gold.skia.org/img/images/[imageHash].png
|
||||
List<List<int>> imageResponseTemplate() {
|
||||
return <List<int>>[
|
||||
<int>[137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73,
|
||||
72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0],
|
||||
<int>[0, 0, 11, 73, 68, 65, 84, 120, 1, 99, 97, 0, 2, 0,
|
||||
0, 25, 0, 5, 144, 240, 54, 245, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96,
|
||||
130],
|
||||
];
|
||||
}
|
212
packages/flutter_goldens_client/lib/client.dart
Normal file
212
packages/flutter_goldens_client/lib/client.dart
Normal file
|
@ -0,0 +1,212 @@
|
|||
// Copyright 2018 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:io' as io;
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:process/process.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
|
||||
|
||||
const String _kFlutterRootKey = 'FLUTTER_ROOT';
|
||||
|
||||
/// 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(),
|
||||
this.process = const LocalProcessManager(),
|
||||
});
|
||||
|
||||
/// The file system to use for storing the local clone of the repository.
|
||||
///
|
||||
/// This is useful in tests, where a local file system (the default) can
|
||||
/// be replaced by a memory file system.
|
||||
final FileSystem fs;
|
||||
|
||||
/// A wrapper for the [dart:io.Platform] API.
|
||||
///
|
||||
/// This is useful in tests, where the system platform (the default) can
|
||||
/// be replaced by a mock platform instance.
|
||||
final Platform platform;
|
||||
|
||||
/// A controller for launching subprocesses.
|
||||
///
|
||||
/// This is useful in tests, where the real process manager (the default)
|
||||
/// can be replaced by a mock process manager that doesn't really create
|
||||
/// subprocesses.
|
||||
final ProcessManager process;
|
||||
|
||||
/// 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 files are located.
|
||||
///
|
||||
/// Uses the [fs] file system.
|
||||
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.
|
||||
///
|
||||
/// This ensures that the goldens repository has been cloned into its
|
||||
/// expected location within `bin/cache` and that it is synced to the Git
|
||||
/// revision specified in `bin/internal/goldens.version`.
|
||||
///
|
||||
/// While this is preparing the repository, it obtains a file lock such that
|
||||
/// [GoldensClient] instances in other processes or isolates will not
|
||||
/// duplicate the work that this is doing.
|
||||
Future<void> prepare() async {
|
||||
final String goldensCommit = await _getGoldensCommit();
|
||||
String currentCommit = await _getCurrentCommit();
|
||||
if (currentCommit != goldensCommit) {
|
||||
await _obtainLock();
|
||||
try {
|
||||
// Check the current commit again now that we have the lock.
|
||||
currentCommit = await _getCurrentCommit();
|
||||
if (currentCommit != goldensCommit) {
|
||||
if (currentCommit == null) {
|
||||
await _initRepository();
|
||||
}
|
||||
await _checkCanSync();
|
||||
await _syncTo(goldensCommit);
|
||||
}
|
||||
} finally {
|
||||
await _releaseLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getCurrentCommit() async {
|
||||
if (!comparisonRoot.existsSync()) {
|
||||
return null;
|
||||
} else {
|
||||
final io.ProcessResult revParse = await process.run(
|
||||
<String>['git', 'rev-parse', 'HEAD'],
|
||||
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 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: comparisonRoot,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _checkCanSync() async {
|
||||
final io.ProcessResult result = await process.run(
|
||||
<String>['git', 'status', '--porcelain'],
|
||||
workingDirectory: comparisonRoot.path,
|
||||
);
|
||||
if (result.stdout.trim().isNotEmpty) {
|
||||
final StringBuffer buf = StringBuffer()
|
||||
..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 ${comparisonRoot.path}')
|
||||
..writeln('git reset --hard HEAD')
|
||||
..writeln('git clean -x -d -f -f');
|
||||
throw NonZeroExitCode(1, buf.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _syncTo(String commit) async {
|
||||
await _runCommands(
|
||||
<String>[
|
||||
'git pull upstream master',
|
||||
'git fetch upstream $commit',
|
||||
'git reset --hard FETCH_HEAD',
|
||||
],
|
||||
workingDirectory: comparisonRoot,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _runCommands(
|
||||
List<String> commands, {
|
||||
Directory workingDirectory,
|
||||
}) async {
|
||||
for (String command in commands) {
|
||||
final List<String> parts = command.split(' ');
|
||||
final io.ProcessResult result = await process.run(
|
||||
parts,
|
||||
workingDirectory: workingDirectory?.path,
|
||||
);
|
||||
if (result.exitCode != 0) {
|
||||
throw NonZeroExitCode(result.exitCode, result.stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _obtainLock() async {
|
||||
final File lockFile = flutterRoot.childFile(fs.path.join('bin', 'cache', 'goldens.lockfile'));
|
||||
await lockFile.create(recursive: true);
|
||||
_lock = await lockFile.open(mode: io.FileMode.write);
|
||||
await _lock.lock(io.FileLock.blockingExclusive);
|
||||
}
|
||||
|
||||
Future<void> _releaseLock() async {
|
||||
await _lock.close();
|
||||
_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.
|
||||
///
|
||||
/// The first argument must be non-zero.
|
||||
const NonZeroExitCode(this.exitCode, this.stderr) : assert(exitCode != 0);
|
||||
|
||||
/// The code that the process will signal to the operating system.
|
||||
///
|
||||
/// By definition, this is not zero.
|
||||
final int exitCode;
|
||||
|
||||
/// The message to show on standard error.
|
||||
final String stderr;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Exit code $exitCode: $stderr';
|
||||
}
|
||||
}
|
|
@ -12,50 +12,30 @@ 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
|
||||
|
||||
const String _kFlutterRootKey = 'FLUTTER_ROOT';
|
||||
// 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';
|
||||
|
||||
/// A client for uploading image tests and making baseline requests to the
|
||||
/// Flutter Gold Dashboard.
|
||||
class SkiaGoldClient {
|
||||
SkiaGoldClient(
|
||||
this.workDirectory, {
|
||||
this.fs = const LocalFileSystem(),
|
||||
this.process = const LocalProcessManager(),
|
||||
this.platform = const LocalPlatform(),
|
||||
io.HttpClient httpClient,
|
||||
}) : assert(workDirectory != null),
|
||||
assert(fs != null),
|
||||
assert(process != null),
|
||||
assert(platform != null),
|
||||
httpClient = httpClient ?? io.HttpClient();
|
||||
|
||||
/// The file system to use for storing the local clone of the repository.
|
||||
///
|
||||
/// This is useful in tests, where a local file system (the default) can
|
||||
/// be replaced by a memory file system.
|
||||
final FileSystem fs;
|
||||
|
||||
/// A wrapper for the [dart:io.Platform] API.
|
||||
///
|
||||
/// This is useful in tests, where the system platform (the default) can
|
||||
/// be replaced by a mock platform instance.
|
||||
final Platform platform;
|
||||
|
||||
/// A controller for launching sub-processes.
|
||||
///
|
||||
/// This is useful in tests, where the real process manager (the default)
|
||||
/// can be replaced by a mock process manager that doesn't really create
|
||||
/// sub-processes.
|
||||
final ProcessManager process;
|
||||
|
||||
/// A client for making Http requests to the Flutter Gold dashboard.
|
||||
final io.HttpClient httpClient;
|
||||
/// 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
|
||||
|
@ -63,21 +43,7 @@ class SkiaGoldClient {
|
|||
///
|
||||
/// This is informed by the [FlutterGoldenFileComparator] [basedir]. It cannot
|
||||
/// be null.
|
||||
final Directory workDirectory;
|
||||
|
||||
/// A map of known golden file tests and their associated positive image
|
||||
/// hashes.
|
||||
///
|
||||
/// This is set and used by the [FlutterLocalFileComparator] and
|
||||
/// [FlutterPreSubmitFileComparator] to test against golden masters maintained
|
||||
/// in the Flutter Gold dashboard.
|
||||
Map<String, List<String>> get expectations => _expectations;
|
||||
Map<String, List<String>> _expectations;
|
||||
|
||||
/// The local [Directory] where the Flutter repository is hosted.
|
||||
///
|
||||
/// Uses the [fs] file system.
|
||||
Directory get _flutterRoot => fs.directory(platform.environment[_kFlutterRootKey]);
|
||||
Directory _workDirectory;
|
||||
|
||||
/// The path to the local [Directory] where the goldctl tool is hosted.
|
||||
///
|
||||
|
@ -90,6 +56,9 @@ class SkiaGoldClient {
|
|||
/// 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.
|
||||
///
|
||||
|
@ -100,31 +69,39 @@ class SkiaGoldClient {
|
|||
/// 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() async {
|
||||
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.');
|
||||
final StringBuffer buf = StringBuffer()..writeln('Gold service account is unavailable.');
|
||||
throw NonZeroExitCode(1, buf.toString());
|
||||
}
|
||||
|
||||
final File authorization = workDirectory.childFile('serviceAccount.json');
|
||||
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,
|
||||
'--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.
|
||||
|
@ -132,8 +109,8 @@ class SkiaGoldClient {
|
|||
/// 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');
|
||||
final File keys = _workDirectory.childFile('keys.json');
|
||||
final File failures = _workDirectory.childFile('failures.json');
|
||||
|
||||
await keys.writeAsString(_getKeysJSON());
|
||||
await failures.create();
|
||||
|
@ -142,9 +119,7 @@ class SkiaGoldClient {
|
|||
final List<String> imgtestInitArguments = <String>[
|
||||
'imgtest', 'init',
|
||||
'--instance', 'flutter',
|
||||
'--work-dir', workDirectory
|
||||
.childDirectory('temp')
|
||||
.path,
|
||||
'--work-dir', _workDirectory.childDirectory('temp').path,
|
||||
'--commit', commitHash,
|
||||
'--keys-file', keys.path,
|
||||
'--failure-file', failures.path,
|
||||
|
@ -152,16 +127,26 @@ class SkiaGoldClient {
|
|||
];
|
||||
|
||||
if (imgtestInitArguments.contains(null)) {
|
||||
final StringBuffer buf = StringBuffer()
|
||||
..writeln('Null argument for Skia Gold imgtest init:');
|
||||
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.
|
||||
|
@ -179,10 +164,8 @@ class SkiaGoldClient {
|
|||
|
||||
final List<String> imgtestArguments = <String>[
|
||||
'imgtest', 'add',
|
||||
'--work-dir', workDirectory
|
||||
.childDirectory('temp')
|
||||
.path,
|
||||
'--test-name', cleanTestName(testName),
|
||||
'--work-dir', _workDirectory.childDirectory('temp').path,
|
||||
'--test-name', testName.split(path.extension(testName.toString()))[0],
|
||||
'--png-file', goldenFile.path,
|
||||
];
|
||||
|
||||
|
@ -190,145 +173,25 @@ class SkiaGoldClient {
|
|||
_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;
|
||||
}
|
||||
|
||||
/// Requests and sets the [_expectations] known to Flutter Gold at head.
|
||||
Future<void> getExpectations() async {
|
||||
_expectations = <String, List<String>>{};
|
||||
await io.HttpOverrides.runWithHttpOverrides<Future<void>>(() async {
|
||||
final Uri requestForExpectations = Uri.parse(
|
||||
'https://flutter-gold.skia.org/json/expectations/commit/HEAD'
|
||||
);
|
||||
String rawResponse;
|
||||
try {
|
||||
final io.HttpClientRequest request = await httpClient.getUrl(requestForExpectations);
|
||||
final io.HttpClientResponse response = await request.close();
|
||||
rawResponse = await utf8.decodeStream(response);
|
||||
final Map<String, dynamic> skiaJson = json.decode(rawResponse)['master'];
|
||||
|
||||
skiaJson.forEach((String key, dynamic value) {
|
||||
final Map<String, dynamic> hashesMap = value;
|
||||
_expectations[key] = hashesMap.keys.toList();
|
||||
});
|
||||
} on FormatException catch(_) {
|
||||
print('Formatting error detected requesting expectations from Flutter Gold.\n'
|
||||
'rawResponse: $rawResponse');
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
SkiaGoldHttpOverrides(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a list of bytes representing the golden image retrieved from the
|
||||
/// Flutter Gold dashboard.
|
||||
///
|
||||
/// The provided image hash represents an expectation from Flutter Gold.
|
||||
Future<List<int>>getImageBytes(String imageHash) async {
|
||||
final List<int> imageBytes = <int>[];
|
||||
await io.HttpOverrides.runWithHttpOverrides<Future<void>>(() async {
|
||||
final Uri requestForImage = Uri.parse(
|
||||
'https://flutter-gold.skia.org/img/images/$imageHash.png',
|
||||
);
|
||||
|
||||
try {
|
||||
final io.HttpClientRequest request = await httpClient.getUrl(requestForImage);
|
||||
final io.HttpClientResponse response = await request.close();
|
||||
await response.forEach((List<int> bytes) => imageBytes.addAll(bytes));
|
||||
|
||||
} catch(e) {
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
SkiaGoldHttpOverrides(),
|
||||
);
|
||||
return imageBytes;
|
||||
}
|
||||
|
||||
/// Returns a boolean value for whether or not the given test and current pull
|
||||
/// request are ignored on Flutter Gold.
|
||||
///
|
||||
/// This is only relevant when used by the [FlutterPreSubmitFileComparator].
|
||||
/// In order to land a change to an exiting golden file, an ignore must be set
|
||||
/// up in Flutter Gold. This will serve as a flag to permit the change to
|
||||
/// land, and protect against any unwanted changes.
|
||||
Future<bool> testIsIgnoredForPullRequest(String pullRequest, String testName) async {
|
||||
bool ignoreIsActive = false;
|
||||
testName = cleanTestName(testName);
|
||||
String rawResponse;
|
||||
await io.HttpOverrides.runWithHttpOverrides<Future<void>>(() async {
|
||||
final Uri requestForIgnores = Uri.parse(
|
||||
'https://flutter-gold.skia.org/json/ignores'
|
||||
);
|
||||
|
||||
try {
|
||||
final io.HttpClientRequest request = await httpClient.getUrl(requestForIgnores);
|
||||
final io.HttpClientResponse response = await request.close();
|
||||
rawResponse = await utf8.decodeStream(response);
|
||||
final List<dynamic> ignores = json.decode(rawResponse);
|
||||
for(Map<String, dynamic> ignore in ignores) {
|
||||
final List<String> ignoredQueries = ignore['query'].split('&');
|
||||
final String ignoredPullRequest = ignore['note'].split('/').last;
|
||||
if (ignoredQueries.contains('name=$testName') &&
|
||||
ignoredPullRequest == pullRequest) {
|
||||
ignoreIsActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} on FormatException catch(_) {
|
||||
print('Formatting error detected requesting ignores from Flutter Gold.\n'
|
||||
'rawResponse: $rawResponse');
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
SkiaGoldHttpOverrides(),
|
||||
);
|
||||
return ignoreIsActive;
|
||||
}
|
||||
|
||||
/// The [_expectations] retrieved from Flutter Gold do not include the
|
||||
/// parameters of the given test. This function queries the Flutter Gold
|
||||
/// details api to determine if the given expectation for a test matches the
|
||||
/// configuration of the executing machine.
|
||||
Future<bool> isValidDigestForExpectation(String expectation, String testName) async {
|
||||
bool isValid = false;
|
||||
testName = cleanTestName(testName);
|
||||
String rawResponse;
|
||||
await io.HttpOverrides.runWithHttpOverrides<Future<void>>(() async {
|
||||
final Uri requestForDigest = Uri.parse(
|
||||
'https://flutter-gold.skia.org/json/details?test=$testName&digest=$expectation'
|
||||
);
|
||||
|
||||
try {
|
||||
final io.HttpClientRequest request = await httpClient.getUrl(requestForDigest);
|
||||
final io.HttpClientResponse response = await request.close();
|
||||
rawResponse = await utf8.decodeStream(response);
|
||||
final Map<String, dynamic> skiaJson = json.decode(rawResponse);
|
||||
final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
|
||||
isValid = digest.isValid(platform, testName, expectation);
|
||||
|
||||
} on FormatException catch(_) {
|
||||
print('Formatting error detected requesting digest from Flutter Gold.\n'
|
||||
'rawResponse: $rawResponse');
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
SkiaGoldHttpOverrides(),
|
||||
);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/// Returns the current commit hash of the Flutter repository.
|
||||
Future<String> _getCurrentCommit() async {
|
||||
if (!_flutterRoot.existsSync()) {
|
||||
if (!flutterRoot.existsSync()) {
|
||||
final StringBuffer buf = StringBuffer()
|
||||
..writeln('Flutter root could not be found: $_flutterRoot');
|
||||
..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,
|
||||
workingDirectory: flutterRoot.path,
|
||||
);
|
||||
return revParse.exitCode == 0 ? revParse.stdout.trim() : null;
|
||||
}
|
||||
|
@ -347,85 +210,13 @@ class SkiaGoldClient {
|
|||
);
|
||||
}
|
||||
|
||||
/// Removes the file extension from the [fileName] to represent the test name
|
||||
/// properly.
|
||||
String cleanTestName(String fileName) {
|
||||
return fileName.split(path.extension(fileName.toString()))[0];
|
||||
}
|
||||
|
||||
/// Returns a boolean value to prevent the client from re-authorizing itself
|
||||
/// for multiple tests.
|
||||
bool _clientIsAuthorized() {
|
||||
final File authFile = workDirectory?.childFile(fs.path.join(
|
||||
final File authFile = _workDirectory?.childFile(super.fs.path.join(
|
||||
'temp',
|
||||
'auth_opt.json',
|
||||
));
|
||||
return authFile.existsSync();
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to make HttpRequests during testing.
|
||||
class SkiaGoldHttpOverrides extends io.HttpOverrides {}
|
||||
|
||||
/// A digest returned from a request to the Flutter Gold dashboard.
|
||||
class SkiaGoldDigest {
|
||||
const SkiaGoldDigest({
|
||||
this.imageHash,
|
||||
this.paramSet,
|
||||
this.testName,
|
||||
this.status,
|
||||
});
|
||||
|
||||
/// Create a digest from requested json.
|
||||
factory SkiaGoldDigest.fromJson(Map<String, dynamic> json) {
|
||||
if (json == null)
|
||||
return null;
|
||||
|
||||
return SkiaGoldDigest(
|
||||
imageHash: json['digest'],
|
||||
paramSet: Map<String, dynamic>.from(json['paramset'] ??
|
||||
<String, String>{'Platform': 'none'}),
|
||||
testName: json['test'],
|
||||
status: json['status'],
|
||||
);
|
||||
}
|
||||
|
||||
/// Unique identifier for the image associated with the digest.
|
||||
final String imageHash;
|
||||
|
||||
/// Parameter set for the given test, e.g. Platform : Windows.
|
||||
final Map<String, dynamic> paramSet;
|
||||
|
||||
/// Test name associated with the digest, e.g. positive or untriaged.
|
||||
final String testName;
|
||||
|
||||
/// Status of the given digest, e.g. positive or untriaged.
|
||||
final String status;
|
||||
|
||||
/// Validates a given digest against the current testing conditions.
|
||||
bool isValid(Platform platform, String name, String expectation) {
|
||||
return imageHash == expectation
|
||||
&& paramSet['Platform'].contains(platform.operatingSystem)
|
||||
&& testName == name
|
||||
&& status == 'positive';
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// The first argument must be non-zero.
|
||||
const NonZeroExitCode(this.exitCode, this.stderr) : assert(exitCode != 0);
|
||||
|
||||
/// The code that the process will signal to the operating system.
|
||||
///
|
||||
/// By definition, this is not zero.
|
||||
final int exitCode;
|
||||
|
||||
/// The message to show on standard error.
|
||||
final String stderr;
|
||||
|
||||
@override
|
||||
String toString() => 'Exit code $exitCode: $stderr';
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@ import 'goldens.dart';
|
|||
|
||||
/// The default [GoldenFileComparator] implementation for `flutter test`.
|
||||
///
|
||||
/// The term __golden file__ refers to a master image that is considered the
|
||||
/// true rendering of a given widget, state, application, or other visual
|
||||
/// The term __golden file__ refers to a master image that is considered the true
|
||||
/// rendering of a given widget, state, application, or other visual
|
||||
/// representation you have chosen to capture. This comparator loads golden
|
||||
/// files from the local file system, treating the golden key as a relative
|
||||
/// path from the test file's directory.
|
||||
|
@ -53,7 +53,7 @@ import 'goldens.dart';
|
|||
/// implements.
|
||||
/// * [matchesGoldenFile], the function from [flutter_test] that invokes the
|
||||
/// comparator.
|
||||
class LocalFileComparator extends GoldenFileComparator with LocalComparisonOutput {
|
||||
class LocalFileComparator extends GoldenFileComparator {
|
||||
/// Creates a new [LocalFileComparator] for the specified [testFile].
|
||||
///
|
||||
/// Golden file keys will be interpreted as file paths relative to the
|
||||
|
@ -90,18 +90,24 @@ class LocalFileComparator extends GoldenFileComparator with LocalComparisonOutpu
|
|||
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
|
||||
final File goldenFile = _getGoldenFile(golden);
|
||||
if (!goldenFile.existsSync()) {
|
||||
throw test_package.TestFailure(
|
||||
'Could not be compared against non-existent file: "$golden"'
|
||||
);
|
||||
throw test_package.TestFailure('Could not be compared against non-existent file: "$golden"');
|
||||
}
|
||||
final List<int> goldenBytes = await goldenFile.readAsBytes();
|
||||
final ComparisonResult result = GoldenFileComparator.compareLists(
|
||||
imageBytes,
|
||||
goldenBytes,
|
||||
);
|
||||
final ComparisonResult result = GoldenFileComparator.compareLists(imageBytes, goldenBytes);
|
||||
|
||||
if (!result.passed) {
|
||||
generateFailureOutput(result, golden, basedir);
|
||||
String additionalFeedback = '';
|
||||
if (result.diffs != null) {
|
||||
additionalFeedback = '\nFailure feedback can be found at ${path.join(basedir.path, 'failures')}';
|
||||
final Map<String, Object> diffs = result.diffs;
|
||||
diffs.forEach((String name, Object untypedImage) {
|
||||
final Image image = untypedImage;
|
||||
final File output = _getFailureFile(name, golden);
|
||||
output.parent.createSync(recursive: true);
|
||||
output.writeAsBytesSync(encodePng(image));
|
||||
});
|
||||
}
|
||||
throw test_package.TestFailure('Golden "$golden": ${result.error}$additionalFeedback');
|
||||
}
|
||||
return result.passed;
|
||||
}
|
||||
|
@ -116,50 +122,16 @@ class LocalFileComparator extends GoldenFileComparator with LocalComparisonOutpu
|
|||
File _getGoldenFile(Uri golden) {
|
||||
return File(_path.join(_path.fromUri(basedir), _path.fromUri(golden.path)));
|
||||
}
|
||||
}
|
||||
|
||||
/// A class for use in golden file comparators that run locally and provide
|
||||
/// output.
|
||||
class LocalComparisonOutput {
|
||||
/// Writes out diffs from the [ComparisonResult] of a golden file test.
|
||||
///
|
||||
/// Will throw an error if a null result is provided.
|
||||
void generateFailureOutput(
|
||||
ComparisonResult result,
|
||||
Uri golden,
|
||||
Uri basedir, {
|
||||
String key = '',
|
||||
}) {
|
||||
String additionalFeedback = '';
|
||||
if (result.diffs != null) {
|
||||
additionalFeedback = '\nFailure feedback can be found at '
|
||||
'${path.join(basedir.path, 'failures')}';
|
||||
final Map<String, Image> diffs = result.diffs;
|
||||
diffs.forEach((String name, Image image) {
|
||||
final File output = getFailureFile(
|
||||
key.isEmpty ? name : name + '_' + key,
|
||||
golden,
|
||||
basedir,
|
||||
);
|
||||
output.parent.createSync(recursive: true);
|
||||
output.writeAsBytesSync(encodePng(image));
|
||||
});
|
||||
}
|
||||
throw test_package.TestFailure(
|
||||
'Golden "$golden": ${result.error}$additionalFeedback'
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the appropriate file for a given diff from a [ComparisonResult].
|
||||
File getFailureFile(String failure, Uri golden, Uri basedir) {
|
||||
File _getFailureFile(String failure, Uri golden) {
|
||||
final String fileName = golden.pathSegments[0];
|
||||
final String testName = fileName.split(path.extension(fileName))[0]
|
||||
+ '_'
|
||||
+ failure
|
||||
+ '.png';
|
||||
return File(path.join(
|
||||
path.fromUri(basedir),
|
||||
path.fromUri(Uri.parse('failures/$testName')),
|
||||
return File(_path.join(
|
||||
_path.fromUri(basedir),
|
||||
_path.fromUri(Uri.parse('failures/$testName')),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -231,9 +203,7 @@ ComparisonResult compareLists(List<int> test, List<int> master) {
|
|||
if (pixelDiffCount > 0) {
|
||||
return ComparisonResult(
|
||||
passed: false,
|
||||
error: 'Pixel test failed, '
|
||||
'${((pixelDiffCount/totalPixels) * 100).toStringAsFixed(2)}% '
|
||||
'diff detected.',
|
||||
error: 'Pixel test failed, ${((pixelDiffCount/totalPixels) * 100).toStringAsFixed(2)}% diff detected.',
|
||||
diffs: diffs,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -303,7 +303,9 @@ Matcher coversSameAreaAs(Path expectedPath, { @required Rect areaToCompare, int
|
|||
/// The [key] may be either a [Uri] or a [String] representation of a URI.
|
||||
///
|
||||
/// The [version] is a number that can be used to differentiate historical
|
||||
/// golden files. This parameter is optional.
|
||||
/// golden files. This parameter is optional. 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).
|
||||
///
|
||||
/// This is an asynchronous matcher, meaning that callers should use
|
||||
/// [expectLater] when using this matcher and await the future returned by
|
||||
|
@ -334,11 +336,8 @@ Matcher coversSameAreaAs(Path expectedPath, { @required Rect areaToCompare, int
|
|||
///
|
||||
/// await expectLater(
|
||||
/// imageFuture,
|
||||
/// matchesGoldenFile(
|
||||
/// 'save.png',
|
||||
/// version: 2,
|
||||
/// ),
|
||||
/// );
|
||||
/// matchesGoldenFile('save.png'),
|
||||
/// );
|
||||
///
|
||||
/// await expectLater(
|
||||
/// find.byType(MyWidget),
|
||||
|
|
Loading…
Reference in a new issue