From 2a5452433762f422d5bfd5a03020c379945571ad Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 12 Apr 2017 13:33:02 -0700 Subject: [PATCH] Fix tests to use Ahem, and helpful changes around that (#9332) * Fix tests to use Ahem, and helpful changes around that - Fix fonts that had metric-specific behaviours. - LiveTestWidgetsFlutterBinding.allowAllFrames has been renamed to LiveTestWidgetsFlutterBinding.framePolicy. - LiveTestWidgetsFlutterBinding now defaults to using a frame policy that pumps slightly more frames, to animate the pointer crosshairs. - Added "flutter run --use-test-fonts" to enable Ahem on devices. - Changed how idle() works to be more effective in live mode. - Display the test name in live mode (unless ahem fonts are enabled). - Added a toString to TextSelectionPoint. - Style nit fixes. * Roll engine to get Ahem changes. * Update tests for dartdoc changes. * Fix flutter_tools tests --- bin/internal/engine.version | 2 +- .../test_async_utils_guarded_expectation.txt | 3 +- ...test_async_utils_unguarded_expectation.txt | 3 +- .../lib/stocks/build_bench.dart | 2 +- .../lib/stocks/layout_bench.dart | 2 +- dev/tools/dartdoc.dart | 6 +- .../test/calculator/smoke_test.dart | 2 +- .../test/example_code_display_test.dart | 2 +- examples/flutter_gallery/test/pesto_test.dart | 2 +- .../test/simple_smoke_test.dart | 2 +- .../flutter_gallery/test/update_test.dart | 2 +- .../flutter/lib/src/rendering/editable.dart | 11 ++ .../test/material/text_field_test.dart | 93 ++++----- .../test/rendering/paragraph_test.dart | 7 +- .../test/widgets/scrollable_fling_test.dart | 54 +++--- packages/flutter_test/lib/src/binding.dart | 179 ++++++++++++++---- .../lib/src/test_async_utils.dart | 6 +- .../flutter_test/lib/src/test_text_input.dart | 3 +- .../flutter_test/lib/src/widget_tester.dart | 49 +++-- .../lib/src/android/android_device.dart | 7 +- .../lib/src/commands/daemon.dart | 40 ++-- .../lib/src/commands/fuchsia_reload.dart | 172 ++++++++--------- .../flutter_tools/lib/src/commands/run.dart | 58 +++--- packages/flutter_tools/lib/src/device.dart | 3 + .../flutter_tools/lib/src/ios/devices.dart | 3 + .../flutter_tools/lib/src/ios/simulators.dart | 2 + .../lib/src/resident_runner.dart | 3 +- packages/flutter_tools/lib/src/run_cold.dart | 3 +- 28 files changed, 435 insertions(+), 286 deletions(-) diff --git a/bin/internal/engine.version b/bin/internal/engine.version index c84d3fc0ed..a56ed9cf2f 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -5d9a6422577d95c242f45f48c47b431f7cf3c548 +1fed16fb25f3f7afc8303116d6ef707c4043c127 diff --git a/dev/automated_tests/flutter_test/test_async_utils_guarded_expectation.txt b/dev/automated_tests/flutter_test/test_async_utils_guarded_expectation.txt index 4601a54d68..e61ce99c73 100644 --- a/dev/automated_tests/flutter_test/test_async_utils_guarded_expectation.txt +++ b/dev/automated_tests/flutter_test/test_async_utils_guarded_expectation.txt @@ -15,7 +15,8 @@ When the exception was thrown, this was the stack: <> \(elided .+\) - +The test description was: +TestAsyncUtils - custom guarded sections ════════════════════════════════════════════════════════════════════════════════════════════════════ .*(this line has more of the test framework's output)? Test failed\. See exception logs above\. diff --git a/dev/automated_tests/flutter_test/test_async_utils_unguarded_expectation.txt b/dev/automated_tests/flutter_test/test_async_utils_unguarded_expectation.txt index de235682d0..1705102e77 100644 --- a/dev/automated_tests/flutter_test/test_async_utils_unguarded_expectation.txt +++ b/dev/automated_tests/flutter_test/test_async_utils_unguarded_expectation.txt @@ -14,7 +14,8 @@ When the exception was thrown, this was the stack: <> (elided [0-9]+ frames from .+) - +The test description was: +TestAsyncUtils - handling unguarded async helper functions ════════════════════════════════════════════════════════════════════════════════════════════════════ .*..:.. \+0 -1: - TestAsyncUtils - handling unguarded async helper functions * Test failed. See exception logs above. diff --git a/dev/benchmarks/microbenchmarks/lib/stocks/build_bench.dart b/dev/benchmarks/microbenchmarks/lib/stocks/build_bench.dart index f3ca9377a0..ed81233e88 100644 --- a/dev/benchmarks/microbenchmarks/lib/stocks/build_bench.dart +++ b/dev/benchmarks/microbenchmarks/lib/stocks/build_bench.dart @@ -22,7 +22,7 @@ Future main() async { // This allows us to call onBeginFrame even when the engine didn't request it, // and have it actually do something: final LiveTestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); - binding.allowAllFrames = true; + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; final Stopwatch watch = new Stopwatch(); int iterations = 0; diff --git a/dev/benchmarks/microbenchmarks/lib/stocks/layout_bench.dart b/dev/benchmarks/microbenchmarks/lib/stocks/layout_bench.dart index b3172102ea..663adc85c6 100644 --- a/dev/benchmarks/microbenchmarks/lib/stocks/layout_bench.dart +++ b/dev/benchmarks/microbenchmarks/lib/stocks/layout_bench.dart @@ -21,7 +21,7 @@ Future main() async { // This allows us to call onBeginFrame even when the engine didn't request it, // and have it actually do something: final LiveTestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); - binding.allowAllFrames = true; + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; final Stopwatch watch = new Stopwatch(); int iterations = 0; diff --git a/dev/tools/dartdoc.dart b/dev/tools/dartdoc.dart index 3ae633aa8a..cc02754f29 100644 --- a/dev/tools/dartdoc.dart +++ b/dev/tools/dartdoc.dart @@ -108,9 +108,9 @@ void createFooter(String footerPath) { void sanityCheckDocs() { final List canaries = [ - '$kDocRoot/api/dart-io/File-class.html', - '$kDocRoot/api/dart-ui/Canvas-class.html', - '$kDocRoot/api/dart-ui/Canvas/drawRect.html', + '$kDocRoot/api/dart.io/File-class.html', + '$kDocRoot/api/dart_ui/Canvas-class.html', + '$kDocRoot/api/dart_ui/Canvas/drawRect.html', '$kDocRoot/api/flutter_test/WidgetTester/pumpWidget.html', '$kDocRoot/api/material/Material-class.html', '$kDocRoot/api/material/Tooltip-class.html', diff --git a/examples/flutter_gallery/test/calculator/smoke_test.dart b/examples/flutter_gallery/test/calculator/smoke_test.dart index 93ed624123..12926177bb 100644 --- a/examples/flutter_gallery/test/calculator/smoke_test.dart +++ b/examples/flutter_gallery/test/calculator/smoke_test.dart @@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); if (binding is LiveTestWidgetsFlutterBinding) - binding.allowAllFrames = true; + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; // We press the "1" and the "2" buttons and check that the display // reads "12". diff --git a/examples/flutter_gallery/test/example_code_display_test.dart b/examples/flutter_gallery/test/example_code_display_test.dart index fcc9142815..32e1922b63 100644 --- a/examples/flutter_gallery/test/example_code_display_test.dart +++ b/examples/flutter_gallery/test/example_code_display_test.dart @@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); if (binding is LiveTestWidgetsFlutterBinding) - binding.allowAllFrames = true; + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; testWidgets('Flutter gallery button example code displays', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/6147 diff --git a/examples/flutter_gallery/test/pesto_test.dart b/examples/flutter_gallery/test/pesto_test.dart index a1c57a2abc..13257ecab0 100644 --- a/examples/flutter_gallery/test/pesto_test.dart +++ b/examples/flutter_gallery/test/pesto_test.dart @@ -9,7 +9,7 @@ import 'package:flutter_gallery/gallery/app.dart'; void main() { final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); if (binding is LiveTestWidgetsFlutterBinding) - binding.allowAllFrames = true; + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; // Regression test for https://github.com/flutter/flutter/pull/5168 testWidgets('Pesto appbar heroics', (WidgetTester tester) async { diff --git a/examples/flutter_gallery/test/simple_smoke_test.dart b/examples/flutter_gallery/test/simple_smoke_test.dart index 7fcbea3dfc..776c5a7bf1 100644 --- a/examples/flutter_gallery/test/simple_smoke_test.dart +++ b/examples/flutter_gallery/test/simple_smoke_test.dart @@ -9,7 +9,7 @@ import 'package:flutter_gallery/main.dart' as flutter_gallery_main; void main() { final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); if (binding is LiveTestWidgetsFlutterBinding) - binding.allowAllFrames = true; + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; testWidgets('Flutter Gallery app simple smoke test', (WidgetTester tester) async { flutter_gallery_main.main(); // builds the app and schedules a frame but doesn't trigger one diff --git a/examples/flutter_gallery/test/update_test.dart b/examples/flutter_gallery/test/update_test.dart index 4cd742bb1d..24cbf114db 100644 --- a/examples/flutter_gallery/test/update_test.dart +++ b/examples/flutter_gallery/test/update_test.dart @@ -13,7 +13,7 @@ Future mockUpdateUrlFetcher() { void main() { final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); if (binding is LiveTestWidgetsFlutterBinding) - binding.allowAllFrames = true; + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; // Regression test for https://github.com/flutter/flutter/pull/5168 testWidgets('update dialog', (WidgetTester tester) async { diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index 75e4b22f3e..306dadbf54 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -40,6 +40,17 @@ class TextSelectionPoint { /// Direction of the text at this edge of the selection. final TextDirection direction; + + @override + String toString() { + switch (direction) { + case TextDirection.ltr: + return '$point-ltr'; + case TextDirection.rtl: + return '$point-rtl'; + } + return '$point'; + } } /// A single line of editable text. diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 412aafcda4..7ae0e72d54 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; @@ -41,12 +40,12 @@ void main() { SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall); const String kThreeLines = - 'First line of text is here abcdef ghijkl mnopqrst. ' + - 'Second line of text goes until abcdef ghijkl mnopq. ' + - 'Third line of stuff keeps going until abcdef ghijk. '; + 'First line of text is ' + + 'Second line goes until ' + + 'Third line of stuff '; const String kFourLines = kThreeLines + - 'Fourth line won\'t display and ends at abcdef ghi. '; + 'Fourth line won\'t display and ends at'; // Returns the first RenderEditable. RenderEditable findRenderEditable(WidgetTester tester) { @@ -69,7 +68,8 @@ void main() { Point textOffsetToPosition(WidgetTester tester, int offset) { final RenderEditable renderEditable = findRenderEditable(tester); final List endpoints = renderEditable.getEndpointsForSelection( - new TextSelection.collapsed(offset: offset)); + new TextSelection.collapsed(offset: offset), + ); expect(endpoints.length, 1); return endpoints[0].point + const Offset(0.0, -2.0); } @@ -102,15 +102,18 @@ void main() { final Size emptyInputSize = inputBox.size; Future checkText(String testValue) async { - await tester.enterText(find.byType(EditableText), testValue); + return TestAsyncUtils.guard(() async { + await tester.enterText(find.byType(EditableText), testValue); - // Check that the onChanged event handler fired. - expect(textFieldValue, equals(testValue)); + // Check that the onChanged event handler fired. + expect(textFieldValue, equals(testValue)); - return await tester.pumpWidget(builder()); + await tester.pumpWidget(builder()); + }); } await checkText(' '); + expect(findTextFieldBox(), equals(inputBox)); expect(inputBox.size, equals(emptyInputSize)); @@ -492,7 +495,7 @@ void main() { await tester.pumpWidget(builder()); final String testValue = kThreeLines; - final String cutValue = 'First line of stuff keeps going until abcdef ghijk. '; + final String cutValue = 'First line of stuff '; await tester.enterText(find.byType(EditableText), testValue); await tester.pumpWidget(builder()); @@ -513,8 +516,8 @@ void main() { await gesture.up(); await tester.pump(); - expect(controller.selection.baseOffset, 76); - expect(controller.selection.extentOffset, 81); + expect(controller.selection.baseOffset, 39); + expect(controller.selection.extentOffset, 44); final RenderEditable renderEditable = findRenderEditable(tester); final List endpoints = renderEditable.getEndpointsForSelection( @@ -531,8 +534,8 @@ void main() { await gesture.up(); await tester.pumpWidget(builder()); - expect(controller.selection.baseOffset, 76); - expect(controller.selection.extentOffset, 108); + expect(controller.selection.baseOffset, 39); + expect(controller.selection.extentOffset, 50); // Drag the left handle to the first line, just after 'First'. handlePos = endpoints[0].point + const Offset(-1.0, 1.0); @@ -545,13 +548,13 @@ void main() { await tester.pumpWidget(builder()); expect(controller.selection.baseOffset, 5); - expect(controller.selection.extentOffset, 108); + expect(controller.selection.extentOffset, 50); await tester.tap(find.text('CUT')); await tester.pumpWidget(builder()); expect(controller.selection.isCollapsed, true); expect(controller.text, cutValue); - }, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961 + }); testWidgets('Can scroll multiline input', (WidgetTester tester) async { final Key textFieldKey = new UniqueKey(); @@ -571,10 +574,12 @@ void main() { } await tester.pumpWidget(builder()); + await tester.pump(const Duration(seconds: 1)); await tester.enterText(find.byType(EditableText), kFourLines); await tester.pumpWidget(builder()); + await tester.pump(const Duration(seconds: 1)); RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); final RenderBox inputBox = findInputBox(); @@ -590,11 +595,11 @@ void main() { TestGesture gesture = await tester.startGesture(firstPos, pointer: 7); await tester.pump(); await gesture.moveBy(const Offset(0.0, -1000.0)); - await tester.pump(const Duration(seconds: 2)); + await tester.pump(const Duration(seconds: 1)); // Wait and drag again to trigger https://github.com/flutter/flutter/issues/6329 // (No idea why this is necessary, but the bug wouldn't repro without it.) await gesture.moveBy(const Offset(0.0, -1000.0)); - await tester.pump(const Duration(seconds: 2)); + await tester.pump(const Duration(seconds: 1)); await gesture.up(); await tester.pump(); @@ -609,27 +614,26 @@ void main() { // Now try scrolling by dragging the selection handle. // Long press the 'i' in 'Fourth line' to select the word. - await tester.pump(const Duration(seconds: 2)); + await tester.pump(const Duration(seconds: 1)); final Point untilPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth line')+8); gesture = await tester.startGesture(untilPos, pointer: 7); - await tester.pump(const Duration(seconds: 2)); + await tester.pump(const Duration(seconds: 1)); await gesture.up(); - await tester.pump(); + await tester.pump(const Duration(seconds: 1)); final RenderEditable renderEditable = findRenderEditable(tester); - final List endpoints = renderEditable.getEndpointsForSelection( - controller.selection); + final List endpoints = renderEditable.getEndpointsForSelection(controller.selection); expect(endpoints.length, 2); // Drag the left handle to the first line, just after 'First'. final Point handlePos = endpoints[0].point + const Offset(-1.0, 1.0); final Point newHandlePos = textOffsetToPosition(tester, kFourLines.indexOf('First') + 5); gesture = await tester.startGesture(handlePos, pointer: 7); - await tester.pump(); + await tester.pump(const Duration(seconds: 1)); await gesture.moveTo(newHandlePos + const Offset(0.0, -10.0)); - await tester.pump(); + await tester.pump(const Duration(seconds: 1)); await gesture.up(); - await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // The text should have scrolled up with the handle to keep the active // cursor visible, back to its original position. @@ -638,7 +642,7 @@ void main() { expect(newFirstPos.y, firstPos.y); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse); - }, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961 + }); testWidgets('InputField smoke test', (WidgetTester tester) async { String textFieldValue; @@ -658,16 +662,18 @@ void main() { await tester.pumpWidget(builder()); - Future checkText(String testValue) async { - await tester.enterText(find.byType(EditableText), testValue); + Future checkText(String testValue) { + return TestAsyncUtils.guard(() async { + await tester.enterText(find.byType(EditableText), testValue); - // Check that the onChanged event handler fired. - expect(textFieldValue, equals(testValue)); + // Check that the onChanged event handler fired. + expect(textFieldValue, equals(testValue)); - return await tester.pumpWidget(builder()); + await tester.pumpWidget(builder()); + }); } - checkText('Hello World'); + await checkText('Hello World'); }); testWidgets('InputField with global key', (WidgetTester tester) async { @@ -691,15 +697,17 @@ void main() { await tester.pumpWidget(builder()); Future checkText(String testValue) async { - await tester.enterText(find.byType(EditableText), testValue); + return TestAsyncUtils.guard(() async { + await tester.enterText(find.byType(EditableText), testValue); - // Check that the onChanged event handler fired. - expect(textFieldValue, equals(testValue)); + // Check that the onChanged event handler fired. + expect(textFieldValue, equals(testValue)); - return await tester.pumpWidget(builder()); + await tester.pumpWidget(builder()); + }); } - checkText('Hello World'); + await checkText('Hello World'); }); testWidgets('TextField with default hintStyle', (WidgetTester tester) async { @@ -929,35 +937,28 @@ void main() { ), ), )); - expect(tester.testTextInput.editingState['text'], isEmpty); await tester.tap(find.byType(TextField)); await tester.pump(); - expect(tester.testTextInput.editingState['text'], equals('Initial Text')); controller.text = 'Updated Text'; await tester.idle(); - expect(tester.testTextInput.editingState['text'], equals('Updated Text')); setState(() { currentController = controller2; }); - await tester.pump(); - expect(tester.testTextInput.editingState['text'], equals('More Text')); controller.text = 'Ignored Text'; await tester.idle(); - expect(tester.testTextInput.editingState['text'], equals('More Text')); controller2.text = 'Final Text'; await tester.idle(); - expect(tester.testTextInput.editingState['text'], equals('Final Text')); }); } diff --git a/packages/flutter/test/rendering/paragraph_test.dart b/packages/flutter/test/rendering/paragraph_test.dart index 9c514c1482..99f19dcdfe 100644 --- a/packages/flutter/test/rendering/paragraph_test.dart +++ b/packages/flutter/test/rendering/paragraph_test.dart @@ -76,7 +76,10 @@ void main() { test('overflow test', () { final RenderParagraph paragraph = new RenderParagraph( - const TextSpan(text: 'This is\na wrapping test. It should wrap at manual newlines, and if softWrap is true, also at spaces.'), + const TextSpan( + text: 'This\n' // 4 characters * 10px font size = 40px width on the first line + 'is a wrapping test. It should wrap at manual newlines, and if softWrap is true, also at spaces.', + style: const TextStyle(fontFamily: 'Ahem', fontSize: 10.0)), maxLines: 1, softWrap: true, ); @@ -90,7 +93,7 @@ void main() { } // Lay out in a narrow box to force wrapping. - layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0)); + layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0)); // enough to fit "This" but not "This is" final double lineHeight = paragraph.size.height; relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.clip); diff --git a/packages/flutter/test/widgets/scrollable_fling_test.dart b/packages/flutter/test/widgets/scrollable_fling_test.dart index 656aee2416..fb788cfa2c 100644 --- a/packages/flutter/test/widgets/scrollable_fling_test.dart +++ b/packages/flutter/test/widgets/scrollable_fling_test.dart @@ -2,21 +2,27 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; - import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; +const TextStyle testFont = const TextStyle( + color: const Color(0xFF00FF00), + fontFamily: 'Ahem', +); + Future pumpTest(WidgetTester tester, TargetPlatform platform) async { await tester.pumpWidget(new Container()); await tester.pumpWidget(new MaterialApp( theme: new ThemeData( - platform: platform + platform: platform, ), - home: new ListView.builder( - itemBuilder: (BuildContext context, int index) { - return new Text('$index'); - }, + home: new Container( + color: const Color(0xFF111111), + child: new ListView.builder( + itemBuilder: (BuildContext context, int index) { + return new Text('$index', style: testFont); + }, + ), ), )); return null; @@ -53,44 +59,44 @@ void main() { final List log = []; final List textWidgets = []; - for (int i = 0; i < 250; i++) - textWidgets.add(new GestureDetector(onTap: () { log.add('tap $i'); }, child: new Text('$i'))); + for (int i = 0; i < 250; i += 1) + textWidgets.add(new GestureDetector(onTap: () { log.add('tap $i'); }, child: new Text('$i', style: testFont))); await tester.pumpWidget(new ListView(children: textWidgets)); expect(log, equals([])); await tester.tap(find.byType(Scrollable)); await tester.pump(const Duration(milliseconds: 50)); - expect(log, equals(['tap 18'])); + expect(log, equals(['tap 21'])); await tester.fling(find.byType(Scrollable), const Offset(0.0, -200.0), 1000.0); await tester.pump(const Duration(milliseconds: 50)); - expect(log, equals(['tap 18'])); + expect(log, equals(['tap 21'])); + await tester.tap(find.byType(Scrollable)); // should stop the fling but not tap anything + await tester.pump(const Duration(milliseconds: 50)); + expect(log, equals(['tap 21'])); await tester.tap(find.byType(Scrollable)); await tester.pump(const Duration(milliseconds: 50)); - expect(log, equals(['tap 18'])); - await tester.tap(find.byType(Scrollable)); - await tester.pump(const Duration(milliseconds: 50)); - expect(log, equals(['tap 18', 'tap 31'])); - }, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961 + expect(log, equals(['tap 21', 'tap 35'])); + }); testWidgets('fling and wait and tap', (WidgetTester tester) async { final List log = []; final List textWidgets = []; - for (int i = 0; i < 250; i++) - textWidgets.add(new GestureDetector(onTap: () { log.add('tap $i'); }, child: new Text('$i'))); + for (int i = 0; i < 250; i += 1) + textWidgets.add(new GestureDetector(onTap: () { log.add('tap $i'); }, child: new Text('$i', style: testFont))); await tester.pumpWidget(new ListView(children: textWidgets)); expect(log, equals([])); await tester.tap(find.byType(Scrollable)); await tester.pump(const Duration(milliseconds: 50)); - expect(log, equals(['tap 18'])); + expect(log, equals(['tap 21'])); await tester.fling(find.byType(Scrollable), const Offset(0.0, -200.0), 1000.0); await tester.pump(const Duration(milliseconds: 50)); - expect(log, equals(['tap 18'])); - await tester.pump(const Duration(seconds: 50)); - expect(log, equals(['tap 18'])); + expect(log, equals(['tap 21'])); + await tester.pump(const Duration(seconds: 50)); // long wait, so the fling will have ended at the end of it + expect(log, equals(['tap 21'])); await tester.tap(find.byType(Scrollable)); await tester.pump(const Duration(milliseconds: 50)); - expect(log, equals(['tap 18', 'tap 42'])); - }, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961 + expect(log, equals(['tap 21', 'tap 48'])); + }); } diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 7bb8ae78e9..3223f94642 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -159,6 +159,9 @@ abstract class TestWidgetsFlutterBinding extends BindingBase /// /// The supplied EnginePhase is the final phase reached during the pump pass; /// if not supplied, the whole pass is executed. + /// + /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how + /// this method works when the test is run with `flutter run`. Future pump([ Duration duration, EnginePhase newPhase = EnginePhase.sendSemanticsTree ]); /// Artificially calls dispatchLocaleChanged on the Widget binding, @@ -175,13 +178,20 @@ abstract class TestWidgetsFlutterBinding extends BindingBase /// Acts as if the application went idle. /// /// Runs all remaining microtasks, including those scheduled as a result of - /// running them, until there are no more microtasks scheduled. + /// running them, until there are no more microtasks scheduled. Then, runs any + /// previously scheduled timers with zero time, and completes the returned future. /// - /// Does not run timers. May result in an infinite loop or run out of memory - /// if microtasks continue to recursively schedule new microtasks. + /// May result in an infinite loop or run out of memory if microtasks continue + /// to recursively schedule new microtasks. Will not run any timers scheduled + /// after this method was invoked, even if they are zero-time timers. Future idle() { - TestAsyncUtils.guardSync(); - return new Future.value(); + return TestAsyncUtils.guard(() { + final Completer completer = new Completer(); + Timer.run(() { + completer.complete(null); + }); + return completer.future; + }); } /// Convert the given point from the global coodinate system (as used by @@ -272,7 +282,11 @@ abstract class TestWidgetsFlutterBinding extends BindingBase /// /// The `invariantTester` argument is called after the `testBody`'s [Future] /// completes. If it throws, then the test is marked as failed. - Future runTest(Future testBody(), VoidCallback invariantTester); + /// + /// The `description` is used by the [LiveTestWidgetsFlutterBinding] to + /// show a label on the screen during the test. The description comes from + /// the value passed to [testWidgets]. It must not be null. + Future runTest(Future testBody(), VoidCallback invariantTester, { String description: '' }); /// This is called during test execution before and after the body has been /// executed. @@ -305,7 +319,8 @@ abstract class TestWidgetsFlutterBinding extends BindingBase _currentTestCompleter.complete(null); } - Future _runTest(Future testBody(), VoidCallback invariantTester) { + Future _runTest(Future testBody(), VoidCallback invariantTester, String description) { + assert(description != null); assert(inTest); _oldExceptionHandler = FlutterError.onError; int _exceptionCount = 0; // number of un-taken exceptions @@ -392,6 +407,8 @@ abstract class TestWidgetsFlutterBinding extends BindingBase information.writeln('At the time of the failure, the widget tree looked as follows:'); information.writeln('# ${treeDump.split("\n").takeWhile((String s) => s != "").join("\n# ")}'); } + if (description.isNotEmpty) + information.writeln('The test description was:\n$description'); } )); assert(_parentZone != null); @@ -514,7 +531,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { @override Future idle() { final Future result = super.idle(); - _fakeAsync.flushMicrotasks(); + _fakeAsync.elapse(const Duration()); return result; } @@ -551,7 +568,8 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { } @override - Future runTest(Future testBody(), VoidCallback invariantTester) { + Future runTest(Future testBody(), VoidCallback invariantTester, { String description: '' }) { + assert(description != null); assert(!inTest); assert(_fakeAsync == null); assert(_clock == null); @@ -560,7 +578,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { Future testBodyResult; _fakeAsync.run((FakeAsync fakeAsync) { assert(fakeAsync == _fakeAsync); - testBodyResult = _runTest(testBody, invariantTester); + testBodyResult = _runTest(testBody, invariantTester, description); assert(inTest); }); // testBodyResult is a Future that was created in the Zone of the fakeAsync. @@ -603,6 +621,35 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { } +/// Available policies for how a [LiveTestWidgetsFlutterBinding] should paint +/// frames. +/// +/// These values are set on the binding's +/// [LiveTestWidgetsFlutterBinding.framePolicy] property. The default is +/// [fadePointers]. +enum LiveTestWidgetsFlutterBindingFramePolicy { + /// Strictly show only frames that are explicitly pumped. This most closely + /// matches the behavior of tests when run under `flutter test`. + onlyPumps, + + /// Show pumped frames, and additionally schedule and run frames to fade + /// out the pointer crosshairs and other debugging information shown by + /// the binding. + /// + /// This can result in additional frames being pumped beyond those that + /// the test itself requests, which can cause differences in behavior. + fadePointers, + + /// Show every frame that the framework requests, even if the frames are not + /// explicitly pumped. + /// + /// This can help with orienting the developer when looking at + /// heavily-animated situations, and will almost certainly result in + /// additional frames being pumped beyond those that the test itself requests, + /// which can cause differences in behavior. + fullyLive, +} + /// A variant of [TestWidgetsFlutterBinding] for executing tests in /// the `flutter run` environment, on a device. This is intended to /// allow interactive test development. @@ -611,13 +658,21 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { /// a device from a development computer, see the [flutter_driver] /// package and the `flutter drive` command. /// -/// This binding overrides the default [SchedulerBinding] behavior to -/// ensure that tests work in the same way in this environment as they -/// would under the [AutomatedTestWidgetsFlutterBinding]. To override -/// this (and see intermediate frames that the test does not -/// explicitly trigger), set [allowAllFrames] to true. (This is likely -/// to make tests fail, though, especially if e.g. they test how many -/// times a particular widget was built.) +/// When running tests using `flutter run`, consider adding the +/// `--use-test-fonts` argument so that the fonts used match those used under +/// `flutter test`. (This forces all text to use the "Ahem" font, which is a +/// font that covers ASCII characters and gives them all the appearance of a +/// square whose size equals the font size.) +/// +/// This binding overrides the default [SchedulerBinding] behavior to ensure +/// that tests work in the same way in this environment as they would under the +/// [AutomatedTestWidgetsFlutterBinding]. To override this (and see intermediate +/// frames that the test does not explicitly trigger), set [framePolicy] to +/// [LiveTestWidgetsFlutterBindingFramePolicy.fullyLive]. (This is likely to +/// make tests fail, though, especially if e.g. they test how many times a +/// particular widget was built.) The default behavior is to show pumped frames +/// and a few additional frames when pointers are triggered (to animate the +/// pointer crosshairs). /// /// This binding does not support the [EnginePhase] argument to /// [pump]. (There would be no point setting it to a value that @@ -644,6 +699,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { Completer _pendingFrame; bool _expectingFrame = false; + bool _viewNeedsPaint = false; /// Whether to have [pump] with a duration only pump a single frame /// (as would happen in a normal test environment using @@ -652,31 +708,46 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { /// asynchronous pause in the test (as would normally happen when /// running an application with [WidgetsFlutterBinding]). /// - /// `false` is the default behavior, which is to only pump once. + /// * [LiveTestWidgetsFlutterBindingFramePolicy.fadePointers] is the default + /// behavior, which is to only pump once, except when there has been some + /// activity with [TestPointer]s, in which case those are shown and may pump + /// additional frames. /// - /// `true` allows all frame requests from the engine to be serviced. + /// * [LiveTestWidgetsFlutterBindingFramePolicy.onlyPumps] is the strictest + /// behavior, which is to only pump once. This most closely matches the + /// [AutomatedTestWidgetsFlutterBinding] (`flutter test`) behavior. /// - /// Setting this to `true` means pumping extra frames, which might - /// involve calling builders more, or calling paint callbacks more, - /// etc, which might interfere with the test. If you know your test - /// file wouldn't be affected by this, you can set it to true - /// persistently in that particular test file. To set this to `true` - /// while still allowing the test file to work as a normal test, add - /// the following code to your test file at the top of your `void - /// main() { }` function, before calls to `testWidgets`: + /// * [LiveTestWidgetsFlutterBindingFramePolicy.fullyLive] allows all frame + /// requests from the engine to be serviced, even those the test did not + /// explicitly pump. + /// + /// Setting this to anything other than + /// [LiveTestWidgetsFlutterBindingFramePolicy.onlyPumps] means pumping extra + /// frames, which might involve calling builders more, or calling paint + /// callbacks more, etc, which might interfere with the test. If you know your + /// test file wouldn't be affected by this, you can set it to + /// [LiveTestWidgetsFlutterBindingFramePolicy.fullyLive] persistently in that + /// particular test file. To set this to + /// [LiveTestWidgetsFlutterBindingFramePolicy.fullyLive] while still allowing + /// the test file to work as a normal test, add the following code to your + /// test file at the top of your `void main() { }` function, before calls to + /// [testWidgets]: /// /// ```dart /// TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); /// if (binding is LiveTestWidgetsFlutterBinding) - /// binding.allowAllFrames = true; + /// binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; /// ``` - bool allowAllFrames = false; + LiveTestWidgetsFlutterBindingFramePolicy framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fadePointers; @override void handleBeginFrame(Duration rawTimeStamp) { - if (_expectingFrame || allowAllFrames) + if (_expectingFrame || + (framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.fullyLive) || + (framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.fadePointers && _viewNeedsPaint)) super.handleBeginFrame(rawTimeStamp); - if (_expectingFrame) { + _viewNeedsPaint = false; + if (_expectingFrame) { // set during pump assert(_pendingFrame != null); _pendingFrame.complete(); // unlocks the test API _pendingFrame = null; @@ -689,13 +760,21 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { @override void initRenderView() { assert(renderView == null); - renderView = new _LiveTestRenderView(configuration: createViewConfiguration()); + renderView = new _LiveTestRenderView( + configuration: createViewConfiguration(), + onNeedPaint: _handleViewNeedsPaint, + ); renderView.scheduleInitialFrame(); } @override _LiveTestRenderView get renderView => super.renderView; + void _handleViewNeedsPaint() { + _viewNeedsPaint = true; + renderView.markNeedsPaint(); + } + /// An object to which real device events should be routed. /// /// Normally, device events are silently dropped. However, if this property is @@ -719,7 +798,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { if (!event.down) renderView._pointers[event.pointer].decay = _kPointerDecay; } - renderView.markNeedsPaint(); + _handleViewNeedsPaint(); super.dispatchEvent(event, result, source: source); break; case TestBindingEventSource.device: @@ -751,10 +830,12 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { } @override - Future runTest(Future testBody(), VoidCallback invariantTester) async { + Future runTest(Future testBody(), VoidCallback invariantTester, { String description: '' }) async { + assert(description != null); assert(!inTest); _inTest = true; - return _runTest(testBody, invariantTester); + renderView._setDescription(description); + return _runTest(testBody, invariantTester, description); } @override @@ -857,7 +938,8 @@ class _LiveTestPointerRecord { class _LiveTestRenderView extends RenderView { _LiveTestRenderView({ - ViewConfiguration configuration + ViewConfiguration configuration, + this.onNeedPaint, }) : super(configuration: configuration); @override @@ -865,8 +947,28 @@ class _LiveTestRenderView extends RenderView { @override set configuration(covariant TestViewConfiguration value) { super.configuration = value; } + final VoidCallback onNeedPaint; + final Map _pointers = {}; + TextPainter _label; + static const TextStyle _labelStyle = const TextStyle( + fontFamily: 'sans-serif', + fontSize: 10.0, + ); + void _setDescription(String value) { + assert(value != null); + if (value.isEmpty) { + _label = null; + return; + } + _label ??= new TextPainter(textAlign: TextAlign.left); + _label.text = new TextSpan(text: value, style: _labelStyle); + _label.layout(); + if (onNeedPaint != null) + onNeedPaint(); + } + @override bool hitTest(HitTestResult result, { Point position }) { final Matrix4 transform = configuration.toHitTestMatrix(); @@ -906,9 +1008,10 @@ class _LiveTestRenderView extends RenderView { .where((int pointer) => _pointers[pointer].decay == 0) .toList() .forEach(_pointers.remove); - if (dirty) - scheduleMicrotask(markNeedsPaint); + if (dirty && onNeedPaint != null) + scheduleMicrotask(onNeedPaint); } + _label?.paint(context.canvas, offset - const Offset(0.0, 10.0)); } } diff --git a/packages/flutter_test/lib/src/test_async_utils.dart b/packages/flutter_test/lib/src/test_async_utils.dart index 100089c6bf..bba63a4acf 100644 --- a/packages/flutter_test/lib/src/test_async_utils.dart +++ b/packages/flutter_test/lib/src/test_async_utils.dart @@ -282,9 +282,13 @@ class TestAsyncUtils { } } + static bool _stripAsynchronousSuspensions(String line) { + return line != ''; + } + static _StackEntry _findResponsibleMethod(StackTrace rawStack, String method, StringBuffer errors) { assert(method == 'guard' || method == 'guardSync'); - final List stack = rawStack.toString().split('\n'); + final List stack = rawStack.toString().split('\n').where(_stripAsynchronousSuspensions).toList(); assert(stack.last == ''); stack.removeLast(); final RegExp getClassPattern = new RegExp(r'^#[0-9]+ +([^. ]+)'); diff --git a/packages/flutter_test/lib/src/test_text_input.dart b/packages/flutter_test/lib/src/test_text_input.dart index e4c3629b09..f7631f8c02 100644 --- a/packages/flutter_test/lib/src/test_text_input.dart +++ b/packages/flutter_test/lib/src/test_text_input.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/services.dart'; @@ -48,7 +49,7 @@ class TestTextInput { [_client, value.toJSON()], ), ), - (_) {}, + (ByteData data) { /* response from framework is discarded */ }, ); } diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart index 8fa900c13c..88e0afe27f 100644 --- a/packages/flutter_test/lib/src/widget_tester.dart +++ b/packages/flutter_test/lib/src/widget_tester.dart @@ -51,7 +51,17 @@ void testWidgets(String description, WidgetTesterCallback callback, { final WidgetTester tester = new WidgetTester._(binding); timeout ??= binding.defaultTestTimeout; test_package.group('-', () { - test_package.test(description, () => binding.runTest(() => callback(tester), tester._endOfTestVerifications), skip: skip); + test_package.test( + description, + () { + return binding.runTest( + () => callback(tester), + tester._endOfTestVerifications, + description: description ?? '', + ); + }, + skip: skip, + ); test_package.tearDown(binding.postTest); }, timeout: timeout); } @@ -109,7 +119,10 @@ Future benchmarkWidgets(WidgetTesterCallback callback) { final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); assert(binding is! AutomatedTestWidgetsFlutterBinding); final WidgetTester tester = new WidgetTester._(binding); - return binding.runTest(() => callback(tester), tester._endOfTestVerifications) ?? new Future.value(); + return binding.runTest( + () => callback(tester), + tester._endOfTestVerifications, + ) ?? new Future.value(); } /// Assert that `actual` matches `matcher`. @@ -163,6 +176,9 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker /// Subsequent calls to this is different from [pump] in that it forces a full /// rebuild of the tree, even if [widget] is the same as the previous call. /// [pump] will only rebuild the widgets that have changed. + /// + /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how + /// this method works when the test is run with `flutter run`. Future pumpWidget(Widget widget, [ Duration duration, EnginePhase phase = EnginePhase.sendSemanticsTree @@ -182,6 +198,9 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker /// /// This is a convenience function that just calls /// [TestWidgetsFlutterBinding.pump]. + /// + /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how + /// this method works when the test is run with `flutter run`. @override Future pump([ Duration duration, @@ -426,23 +445,25 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker /// Tests that just need to add text to widgets like [Input] or [TextField] /// only need to call [enterText]. Future showKeyboard(Finder finder) async { - // TODO(hansmuller): Once find.descendant (#7789) lands replace the following - // RHS with state(find.descendant(finder), find.byType(EditableText)). - final EditableTextState editable = state(finder); - if (editable != binding.focusedEditable) { - binding.focusedEditable = editable; - await pump(); - } - return null; + return TestAsyncUtils.guard(() async { + // TODO(hansmuller): Once find.descendant (#7789) lands replace the following + // RHS with state(find.descendant(finder), find.byType(EditableText)). + final EditableTextState editable = state(finder); + if (editable != binding.focusedEditable) { + binding.focusedEditable = editable; + await pump(); + } + }); } /// Give the EditableText widget specified by [finder] the focus and /// enter [text] as if it been provided by the onscreen keyboard. Future enterText(Finder finder, String text) async { - await showKeyboard(finder); - testTextInput.enterText(text); - await idle(); - return null; + return TestAsyncUtils.guard(() async { + await showKeyboard(finder); + testTextInput.enterText(text); + await idle(); + }); } } diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index 2e1bf5e781..9bc7f225f8 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -337,7 +337,7 @@ class AndroidDevice extends Device { if (debuggingOptions.debuggingEnabled) { // TODO(devoncarew): Remember the forwarding information (so we can later remove the - // port forwarding). + // port forwarding or set it up again when adb fails on us). observatoryDiscovery = new ProtocolDiscovery.observatory( getLogReader(), portForwarder: portForwarder, hostPort: debuggingOptions.observatoryPort); diagnosticDiscovery = new ProtocolDiscovery.diagnosticService( @@ -363,6 +363,8 @@ class AndroidDevice extends Device { cmd.addAll(['--ez', 'enable-checked-mode', 'true']); if (debuggingOptions.startPaused) cmd.addAll(['--ez', 'start-paused', 'true']); + if (debuggingOptions.useTestFonts) + cmd.addAll(['--ez', 'use-test-fonts', 'true']); } cmd.add(apk.launchActivity); final String result = runCheckedSync(cmd); @@ -372,9 +374,8 @@ class AndroidDevice extends Device { return new LaunchResult.failed(); } - if (!debuggingOptions.debuggingEnabled) { + if (!debuggingOptions.debuggingEnabled) return new LaunchResult.succeeded(); - } // Wait for the service protocol port here. This will complete once the // device has printed "Observatory is listening on...". diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 16da917a6f..50483fe87a 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -296,6 +296,7 @@ class AppDomain extends Domain { final String deviceId = _getStringArg(args, 'deviceId', required: true); final String projectDirectory = _getStringArg(args, 'projectDirectory', required: true); final bool startPaused = _getBoolArg(args, 'startPaused') ?? false; + final bool useTestFonts = _getBoolArg(args, 'useTestFonts') ?? false; final String route = _getStringArg(args, 'route'); final String mode = _getStringArg(args, 'mode'); final String target = _getStringArg(args, 'target'); @@ -309,10 +310,25 @@ class AppDomain extends Domain { throw "'$projectDirectory' does not exist"; final BuildMode buildMode = getBuildModeForName(mode) ?? BuildMode.debug; + DebuggingOptions options; + if (buildMode == BuildMode.release) { + options = new DebuggingOptions.disabled(buildMode); + } else { + options = new DebuggingOptions.enabled( + buildMode, + startPaused: startPaused, + useTestFonts: useTestFonts, + ); + } final AppInstance app = await startApp( - device, projectDirectory, target, route, - buildMode, startPaused, enableHotReload); + device, + projectDirectory, + target, + route, + options, + enableHotReload, + ); return { 'appId': app.id, @@ -324,28 +340,14 @@ class AppDomain extends Domain { Future startApp( Device device, String projectDirectory, String target, String route, - BuildMode buildMode, bool startPaused, bool enableHotReload, { + DebuggingOptions options, bool enableHotReload, { String applicationBinary, String projectRootPath, String packagesFilePath, String projectAssets, }) async { - DebuggingOptions options; - - switch (buildMode) { - case BuildMode.debug: - case BuildMode.profile: - options = new DebuggingOptions.enabled(buildMode, startPaused: startPaused); - break; - case BuildMode.release: - options = new DebuggingOptions.disabled(buildMode); - break; - default: - throw 'unhandle build mode: $buildMode'; - } - - if (device.isLocalEmulator && !isEmulatorBuildMode(buildMode)) - throw '${toTitleCase(getModeName(buildMode))} mode is not supported for emulators.'; + if (device.isLocalEmulator && !isEmulatorBuildMode(options.buildMode)) + throw '${toTitleCase(getModeName(options.buildMode))} mode is not supported for emulators.'; // We change the current working directory for the duration of the `start` command. final Directory cwd = fs.currentDirectory; diff --git a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart index dcce29e1e1..e399fc6321 100644 --- a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart +++ b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart @@ -25,21 +25,6 @@ import '../vmservice.dart'; // -g //lib/flutter/examples/flutter_gallery:flutter_gallery class FuchsiaReloadCommand extends FlutterCommand { - String _fuchsiaRoot; - String _projectRoot; - String _projectName; - String _binaryName; - String _fuchsiaProjectPath; - String _target; - String _address; - String _dotPackagesPath; - - @override - final String name = 'fuchsia_reload'; - - @override - final String description = 'Hot reload on Fuchsia.'; - FuchsiaReloadCommand() { addBuildModeFlags(defaultToRelease: false); argParser.addOption('address', @@ -58,7 +43,7 @@ class FuchsiaReloadCommand extends FlutterCommand { help: 'GN target of the application, e.g //path/to/app:app'); argParser.addOption('name-override', abbr: 'n', - help: 'On-device name of the application binary'); + help: 'On-device name of the application binary.'); argParser.addOption('target', abbr: 't', defaultsTo: flx.defaultMainPath, @@ -66,6 +51,21 @@ class FuchsiaReloadCommand extends FlutterCommand { 'Relative to --gn-target path, e.g. lib/main.dart'); } + @override + final String name = 'fuchsia_reload'; + + @override + final String description = 'Hot reload on Fuchsia.'; + + String _fuchsiaRoot; + String _projectRoot; + String _projectName; + String _binaryName; + String _fuchsiaProjectPath; + String _target; + String _address; + String _dotPackagesPath; + @override Future runCommand() async { Cache.releaseLockEarly(); @@ -74,37 +74,34 @@ class FuchsiaReloadCommand extends FlutterCommand { // Find the network ports used on the device by VM service instances. final List servicePorts = await _getServicePorts(); - if (servicePorts.isEmpty) { - throwToolExit("Couldn't find any running Observatory instances."); - } - for (int port in servicePorts) { - printTrace("Fuchsia service port: $port"); - } + if (servicePorts.isEmpty) + throwToolExit('Couldn\'t find any running Observatory instances.'); + for (int port in servicePorts) + printTrace('Fuchsia service port: $port'); // Check that there are running VM services on the returned // ports, and find the Isolates that are running the target app. - final String isolateName = "$_binaryName\$main"; + final String isolateName = '$_binaryName\$main'; final List targetPorts = await _filterPorts(servicePorts, isolateName); - if (targetPorts.isEmpty) { - throwToolExit("No VMs found running $_binaryName"); - } - for (int port in targetPorts) { - printTrace("Found $_binaryName at $port"); - } + if (targetPorts.isEmpty) + throwToolExit('No VMs found running $_binaryName.'); + for (int port in targetPorts) + printTrace('Found $_binaryName at $port'); // Set up a device and hot runner and attach the hot runner to the first // vm service we found. final int firstPort = targetPorts[0]; - final String fullAddress = "$_address:$firstPort"; + final String fullAddress = '$_address:$firstPort'; final FuchsiaDevice device = new FuchsiaDevice(fullAddress); final HotRunner hotRunner = new HotRunner( - device, - debuggingOptions: new DebuggingOptions.enabled(getBuildMode()), - target: _target, - projectRootPath: _fuchsiaProjectPath, - packagesFilePath: _dotPackagesPath); - final Uri observatoryUri = Uri.parse("http://$fullAddress"); - printStatus("Connecting to $_binaryName at $observatoryUri"); + device, + debuggingOptions: new DebuggingOptions.enabled(getBuildMode()), + target: _target, + projectRootPath: _fuchsiaProjectPath, + packagesFilePath: _dotPackagesPath, + ); + final Uri observatoryUri = Uri.parse('http://$fullAddress'); + printStatus('Connecting to $_binaryName at $observatoryUri'); await hotRunner.attach(observatoryUri, isolateFilter: isolateName); } @@ -112,20 +109,19 @@ class FuchsiaReloadCommand extends FlutterCommand { Future> _filterPorts(List ports, String isolateFilter) async { final List result = []; for (int port in ports) { - final String addr = "http://$_address:$port"; + final String addr = 'http://$_address:$port'; final Uri uri = Uri.parse(addr); final VMService vmService = VMService.connect(uri); await vmService.getVM(); await vmService.waitForViews(); if (vmService.vm.firstView == null) { - printTrace("Found no views at $addr"); + printTrace('Found no views at $addr'); continue; } for (FlutterView v in vmService.vm.views) { - printTrace("At $addr, found view: ${v.uiIsolate.name}"); - if (v.uiIsolate.name.indexOf(isolateFilter) == 0) { + printTrace('At $addr, found view: ${v.uiIsolate.name}'); + if (v.uiIsolate.name.indexOf(isolateFilter) == 0) result.add(port); - } } } return result; @@ -133,48 +129,36 @@ class FuchsiaReloadCommand extends FlutterCommand { void _validateArguments() { _fuchsiaRoot = argResults['fuchsia-root']; - if (_fuchsiaRoot == null) { - throwToolExit( - "Please give the location of the Fuchsia tree with --fuchsia-root"); - } - if (!_directoryExists(_fuchsiaRoot)) { - throwToolExit("Specified --fuchsia-root '$_fuchsiaRoot' does not exist"); - } + if (_fuchsiaRoot == null) + throwToolExit('Please give the location of the Fuchsia tree with --fuchsia-root.'); + if (!_directoryExists(_fuchsiaRoot)) + throwToolExit('Specified --fuchsia-root "$_fuchsiaRoot" does not exist.'); _address = argResults['address']; - if (_address == null) { - throwToolExit( - "Give the address of the device running Fuchsia with --address"); - } + if (_address == null) + throwToolExit('Give the address of the device running Fuchsia with --address.'); final List gnTarget = _extractPathAndName(argResults['gn-target']); _projectRoot = gnTarget[0]; _projectName = gnTarget[1]; - _fuchsiaProjectPath = "$_fuchsiaRoot/$_projectRoot"; - if (!_directoryExists(_fuchsiaProjectPath)) { - throwToolExit( - "Target does not exist in the Fuchsia tree: $_fuchsiaProjectPath"); - } + _fuchsiaProjectPath = '$_fuchsiaRoot/$_projectRoot'; + if (!_directoryExists(_fuchsiaProjectPath)) + throwToolExit('Target does not exist in the Fuchsia tree: $_fuchsiaProjectPath.'); final String relativeTarget = argResults['target']; - if (relativeTarget == null) { - throwToolExit('Give the application entry point with --target'); - } - _target = "$_fuchsiaProjectPath/$relativeTarget"; - if (!_fileExists(_target)) { - throwToolExit("Couldn't find application entry point at $_target"); - } + if (relativeTarget == null) + throwToolExit('Give the application entry point with --target.'); + _target = '$_fuchsiaProjectPath/$relativeTarget'; + if (!_fileExists(_target)) + throwToolExit('Couldn\'t find application entry point at $_target.'); final String buildType = argResults['build-type']; - if (buildType == null) { - throwToolExit("Give the build type with --build-type"); - } - final String packagesFileName = "${_projectName}_dart_package.packages"; - _dotPackagesPath = - "$_fuchsiaRoot/out/$buildType/gen/$_projectRoot/$packagesFileName"; - if (!_fileExists(_dotPackagesPath)) { - throwToolExit("Couldn't find .packages file at $_dotPackagesPath"); - } + if (buildType == null) + throwToolExit('Give the build type with --build-type.'); + final String packagesFileName = '${_projectName}_dart_package.packages'; + _dotPackagesPath = '$_fuchsiaRoot/out/$buildType/gen/$_projectRoot/$packagesFileName'; + if (!_fileExists(_dotPackagesPath)) + throwToolExit('Couldn\'t find .packages file at $_dotPackagesPath.'); final String nameOverride = argResults['name-override']; if (nameOverride == null) { @@ -186,26 +170,23 @@ class FuchsiaReloadCommand extends FlutterCommand { List _extractPathAndName(String gnTarget) { final String errorMessage = - "fuchsia_reload --target '$gnTarget' should have the form: " - "'//path/to/app:name'"; + 'fuchsia_reload --target "$gnTarget" should have the form: ' + '"//path/to/app:name"'; // Separate strings like //path/to/target:app into [path/to/target, app] final int lastColon = gnTarget.lastIndexOf(':'); - if (lastColon < 0) { + if (lastColon < 0) throwToolExit(errorMessage); - } final String name = gnTarget.substring(lastColon + 1); // Skip '//' and chop off after : - if ((gnTarget.length < 3) || (gnTarget[0] != '/') || (gnTarget[1] != '/')) { + if ((gnTarget.length < 3) || (gnTarget[0] != '/') || (gnTarget[1] != '/')) throwToolExit(errorMessage); - } final String path = gnTarget.substring(2, lastColon); return [path, name]; } Future> _getServicePorts() async { - final FuchsiaDeviceCommandRunner runner = - new FuchsiaDeviceCommandRunner(_fuchsiaRoot); - final List lsOutput = await runner.run("ls /tmp/dart.services"); + final FuchsiaDeviceCommandRunner runner = new FuchsiaDeviceCommandRunner(_fuchsiaRoot); + final List lsOutput = await runner.run('ls /tmp/dart.services'); final List ports = []; for (String s in lsOutput) { final String trimmed = s.trim(); @@ -213,9 +194,8 @@ class FuchsiaReloadCommand extends FlutterCommand { final String lastWord = trimmed.substring(lastSpace + 1); if ((lastWord != '.') && (lastWord != '..')) { final int value = int.parse(lastWord, onError: (_) => null); - if (value != null) { + if (value != null) ports.add(value); - } } } return ports; @@ -242,26 +222,24 @@ class FuchsiaDeviceCommandRunner { Future> run(String command) async { final int tag = _rng.nextInt(999999); - const String kNetRunCommand = "out/build-magenta/tools/netruncmd"; + const String kNetRunCommand = 'out/build-magenta/tools/netruncmd'; final String netruncmd = fs.path.join(_fuchsiaRoot, kNetRunCommand); - const String kNetCP = "out/build-magenta/tools/netcp"; + const String kNetCP = 'out/build-magenta/tools/netcp'; final String netcp = fs.path.join(_fuchsiaRoot, kNetCP); - final String remoteStdout = "/tmp/netruncmd.$tag"; - final String localStdout = "${fs.systemTempDirectory.path}/netruncmd.$tag"; - final String redirectedCommand = "$command > $remoteStdout"; + final String remoteStdout = '/tmp/netruncmd.$tag'; + final String localStdout = '${fs.systemTempDirectory.path}/netruncmd.$tag'; + final String redirectedCommand = '$command > $remoteStdout'; // Run the command with output directed to a tmp file. ProcessResult result = - await Process.run(netruncmd, [":", redirectedCommand]); - if (result.exitCode != 0) { + await Process.run(netruncmd, [':', redirectedCommand]); + if (result.exitCode != 0) return null; - } // Copy that file to the local filesystem. - result = await Process.run(netcp, [":$remoteStdout", localStdout]); + result = await Process.run(netcp, [':$remoteStdout', localStdout]); // Try to delete the remote file. Don't care about the result; - Process.run(netruncmd, [":", "rm $remoteStdout"]); - if (result.exitCode != 0) { + Process.run(netruncmd, [':', 'rm $remoteStdout']); + if (result.exitCode != 0) return null; - } // Read the local file. final File f = fs.file(localStdout); List lines; diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index e1597804bc..50fe3ed67c 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -88,6 +88,14 @@ class RunCommand extends RunCommandBase { defaultsTo: false, negatable: false, help: 'Start in a paused mode and wait for a debugger to connect.'); + argParser.addFlag('use-test-fonts', + negatable: true, + defaultsTo: false, + help: 'Enable (and default to) the "Ahem" font. This is a special font\n' + 'used in tests to remove any dependencies on the font metrics. It\n' + 'is enabled when you use "flutter test". Set this flag when running\n' + 'a test using "flutter run" for debugging purposes. This flag is\n' + 'only available when running in debug mode.'); argParser.addFlag('build', defaultsTo: true, help: 'If necessary, build the app before running.'); @@ -126,18 +134,19 @@ class RunCommand extends RunCommandBase { hide: !verboseHelp, help: 'Stay resident after launching the application.'); - // Hidden option to enable a benchmarking mode. This will run the given - // application, measure the startup time and the app restart time, write the - // results out to 'refresh_benchmark.json', and exit. This flag is intended - // for use in generating automated flutter benchmarks. - argParser.addFlag('benchmark', negatable: false, hide: !verboseHelp); + argParser.addFlag('benchmark', + negatable: false, + hide: !verboseHelp, + help: 'Enable a benchmarking mode. This will run the given application,\n' + 'measure the startup time and the app restart time, write the\n' + 'results out to "refresh_benchmark.json", and exit. This flag is\n' + 'intended for use in generating automated flutter benchmarks.'); commandValidator = () { - if (!runningWithPrebuiltApplication) - commonCommandValidator(); - // When running with a prebuilt application, no command validation is // necessary. + if (!runningWithPrebuiltApplication) + commonCommandValidator(); }; } @@ -196,6 +205,20 @@ class RunCommand extends RunCommandBase { return super.verifyThenRunCommand(); } + DebuggingOptions _createDebuggingOptions() { + if (getBuildMode() == BuildMode.release) { + return new DebuggingOptions.disabled(getBuildMode()); + } else { + return new DebuggingOptions.enabled( + getBuildMode(), + startPaused: argResults['start-paused'], + useTestFonts: argResults['use-test-fonts'], + observatoryPort: observatoryPort, + diagnosticPort: diagnosticPort, + ); + } + } + @override Future runCommand() async { @@ -212,7 +235,7 @@ class RunCommand extends RunCommandBase { try { app = await daemon.appDomain.startApp( device, fs.currentDirectory.path, targetFile, route, - getBuildMode(), argResults['start-paused'], hotMode, + _createDebuggingOptions(), hotMode, applicationBinary: argResults['use-application-binary'], projectRootPath: argResults['project-root'], packagesFilePath: argResults['packages'], @@ -229,19 +252,6 @@ class RunCommand extends RunCommandBase { if (device.isLocalEmulator && !isEmulatorBuildMode(getBuildMode())) throwToolExit('${toTitleCase(getModeName(getBuildMode()))} mode is not supported for emulators.'); - DebuggingOptions options; - - if (getBuildMode() == BuildMode.release) { - options = new DebuggingOptions.disabled(getBuildMode()); - } else { - options = new DebuggingOptions.enabled( - getBuildMode(), - startPaused: argResults['start-paused'], - observatoryPort: observatoryPort, - diagnosticPort: diagnosticPort, - ); - } - if (hotMode) { if (!device.supportsHotMode) throwToolExit('Hot mode is not supported by this device. Run with --no-hot.'); @@ -258,7 +268,7 @@ class RunCommand extends RunCommandBase { runner = new HotRunner( device, target: targetFile, - debuggingOptions: options, + debuggingOptions: _createDebuggingOptions(), benchmarkMode: argResults['benchmark'], applicationBinary: argResults['use-application-binary'], kernelFilePath: argResults['kernel'], @@ -271,7 +281,7 @@ class RunCommand extends RunCommandBase { runner = new ColdRunner( device, target: targetFile, - debuggingOptions: options, + debuggingOptions: _createDebuggingOptions(), traceStartup: traceStartup, applicationBinary: argResults['use-application-binary'], stayResident: stayResident, diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index a69951ef19..7489153732 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -279,12 +279,14 @@ abstract class Device { class DebuggingOptions { DebuggingOptions.enabled(this.buildMode, { this.startPaused: false, + this.useTestFonts: false, this.observatoryPort, this.diagnosticPort }) : debuggingEnabled = true; DebuggingOptions.disabled(this.buildMode) : debuggingEnabled = false, + useTestFonts = false, startPaused = false, observatoryPort = null, diagnosticPort = null; @@ -293,6 +295,7 @@ class DebuggingOptions { final BuildMode buildMode; final bool startPaused; + final bool useTestFonts; final int observatoryPort; final int diagnosticPort; diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 8f642dbde6..f9cd43b68b 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -228,6 +228,9 @@ class IOSDevice extends Device { if (debuggingOptions.startPaused) launchArguments.add("--start-paused"); + if (debuggingOptions.useTestFonts) + launchArguments.add("--use-test-fonts"); + if (debuggingOptions.debuggingEnabled) { launchArguments.add("--enable-checked-mode"); diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 3ecd761017..fbd99dd679 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -455,6 +455,8 @@ class IOSSimulator extends Device { args.add('--enable-checked-mode'); if (debuggingOptions.startPaused) args.add('--start-paused'); + if (debuggingOptions.useTestFonts) + args.add('--use-test-fonts'); final int observatoryPort = await debuggingOptions.findBestObservatoryPort(); args.add('--observatory-port=$observatoryPort'); diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 94ab4bc366..d843fee676 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -210,9 +210,8 @@ abstract class ResidentRunner { } Future connectToServiceProtocol(Uri uri, {String isolateFilter}) async { - if (!debuggingOptions.debuggingEnabled) { + if (!debuggingOptions.debuggingEnabled) return new Future.error('Error the service protocol is not enabled.'); - } vmService = VMService.connect(uri); printTrace('Connected to service protocol: $uri'); await vmService.getVM(); diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart index 7bd6ced332..a31a3261ff 100644 --- a/packages/flutter_tools/lib/src/run_cold.dart +++ b/packages/flutter_tools/lib/src/run_cold.dart @@ -100,9 +100,8 @@ class ColdRunner extends ResidentRunner { startTime.stop(); // Connect to observatory. - if (debuggingOptions.debuggingEnabled) { + if (debuggingOptions.debuggingEnabled) await connectToServiceProtocol(_result.observatoryUri); - } if (_result.hasObservatory) { connectionInfoCompleter?.complete(new DebugConnectionInfo(