diff --git a/.cirrus.yml b/.cirrus.yml index 69d6f3a088d..5baaa6cd4f6 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -72,7 +72,11 @@ task: env: GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] SHARD: tests + DEPOT_TOOLS: "tmp/depot_tools" + GOLDCTL: "$DEPOT_TOOLS/goldctl" + GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095] SUBSHARD: framework_other + goldctl_script: ./dev/bots/download_goldctl.sh test_script: - dart --enable-asserts ./dev/bots/test.dart container: @@ -219,7 +223,11 @@ task: - ./dev/bots/firebase_testlab.sh - export CIRRUS_CHANGE_MESSAGE=`cat /tmp/cirrus_change_message.txt` - export CIRRUS_COMMIT_MESSAGE=`cat /tmp/cirrus_commit_message.txt` - + - name: customer_testing-linux + script: + - rm -rf bin/cache/pkg/tests + - git clone https://github.com/flutter/tests.git bin/cache/pkg/tests + - dart --enable-asserts dev/customer_testing/run_tests.dart --skip-on-fetch-failure --skip-template bin/cache/pkg/tests/registry/*.test task: use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' && $CIRRUS_PR == '' @@ -229,6 +237,7 @@ task: cpu: 4 env: CIRRUS_WORKING_DIR: "C:\\Windows\\Temp\\flutter sdk" + PATH: "$CIRRUS_WORKING_DIR/bin;$CIRRUS_WORKING_DIR/bin/cache/dart-sdk/bin;$PATH" git_fetch_script: - git clean -xfd - git fetch origin @@ -244,12 +253,12 @@ task: folder: bin\cache\artifacts fingerprint_script: echo %OS% & type bin\internal\engine.version setup_script: - - bin\flutter.bat config --no-analytics - - bin\flutter.bat doctor -v - - bin\flutter.bat update-packages + - flutter config --no-analytics + - flutter doctor -v + - flutter update-packages - git fetch origin master test_all_script: - - bin\cache\dart-sdk\bin\dart.exe --enable-asserts dev\bots\test.dart + - dart --enable-asserts dev\bots\test.dart matrix: # all of the tests except test/integration and test/commands/create_test for packages/flutter_tools - name: tool_tests-windows @@ -279,6 +288,7 @@ task: cpu: 4 env: CIRRUS_WORKING_DIR: "C:\\Windows\\Temp\\flutter sdk" + PATH: "$CIRRUS_WORKING_DIR/bin;$CIRRUS_WORKING_DIR/bin/cache/dart-sdk/bin;$PATH" git_fetch_script: - git clean -xfd - git fetch origin @@ -294,40 +304,56 @@ task: folder: bin\cache\artifacts fingerprint_script: echo %OS% & type bin\internal\engine.version setup_script: - - bin\flutter.bat config --no-analytics - - bin\flutter.bat doctor -v - - bin\flutter.bat update-packages + - flutter config --no-analytics + - flutter doctor -v + - flutter update-packages - git fetch origin master - test_all_script: - - bin\cache\dart-sdk\bin\dart.exe --enable-asserts dev\bots\test.dart matrix: - name: tests_widgets-windows env: GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] SHARD: tests SUBSHARD: widgets + test_all_script: + - dart --enable-asserts dev\bots\test.dart - name: tests_framework_other-windows env: GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] SHARD: tests SUBSHARD: framework_other + GOLDCTL: "C:\\Windows\\Temp\\goldctl_tool\\goldctl.exe" + GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095] + goldctl_script: powershell dev\bots\download_goldctl.ps1 + test_all_script: + - dart --enable-asserts dev\bots\test.dart - name: tests_extras-windows env: GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] SHARD: tests SUBSHARD: extras + test_all_script: + - dart --enable-asserts dev\bots\test.dart - name: build_tests-windows env: SHARD: build_tests container: cpu: 4 memory: 12G + test_all_script: + - dart --enable-asserts dev\bots\test.dart - name: integration_tests-windows env: SHARD: integration_tests container: cpu: 4 memory: 12G + test_all_script: + - dart --enable-asserts dev\bots\test.dart + - name: customer_testing-windows + test_script: + - CMD /S /C "IF EXIST "bin\cache\pkg\tests\" RMDIR /S /Q bin\cache\pkg\tests" + - git clone https://github.com/flutter/tests.git bin\cache\pkg\tests + - dart --enable-asserts dev\customer_testing\run_tests.dart --skip-on-fetch-failure --skip-template bin/cache/pkg/tests/registry/*.test task: use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' @@ -395,9 +421,9 @@ task: - bin/flutter config --no-analytics - bin/flutter doctor -v - bin/flutter update-packages - test_all_script: | - ulimit -S -n 2048 # https://github.com/flutter/flutter/issues/2976 - bin/cache/dart-sdk/bin/dart --enable-asserts dev/bots/test.dart + test_all_script: + - ulimit -S -n 2048 # https://github.com/flutter/flutter/issues/2976 + - bin/cache/dart-sdk/bin/dart --enable-asserts dev/bots/test.dart matrix: # all of the tests except test/integration and test/commands/create_test for packages/flutter_tools - name: tool_tests-macos @@ -428,37 +454,7 @@ task: env: CIRRUS_WORKING_DIR: "/tmp/flutter sdk" COCOAPODS_DISABLE_STATS: true - matrix: - - name: tests_widgets-macos - env: - GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] - SHARD: tests - SUBSHARD: widgets - - name: tests_framework_other-macos - env: - GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] - SHARD: tests - SUBSHARD: framework_other - - name: tests_extras-macos - env: - GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] - SHARD: tests - SUBSHARD: extras - - name: $SHARD-macos - env: - matrix: - # The flakiness of this target has increased beyond tolerable levels. Until we can stabilize it, - # keep the shard disabled. - # - SHARD: integration_tests - - SHARD: build_tests - COCOAPODS_DISABLE_STATS: true - FLUTTER_FRAMEWORK_DIR: "/tmp/flutter sdk/bin/cache/artifacts/engine/ios/" - osx_instance: - image: mojave-flutter - remove_preinstalled_fluuter_script: rm -rf $FLUTTER_HOME - - name: add2app-macos - env: - SHARD: add2app_test + PATH: "$CIRRUS_WORKING_DIR/bin:$CIRRUS_WORKING_DIR/bin/cache/dart-sdk/bin:$PATH" # occasionally the clock on these machines is out of sync # with the actual time - this should help to verify print_date_script: @@ -482,9 +478,57 @@ task: - bin/flutter config --no-analytics - bin/flutter doctor -v - bin/flutter update-packages - test_all_script: | - ulimit -S -n 2048 # https://github.com/flutter/flutter/issues/2976 - bin/cache/dart-sdk/bin/dart --enable-asserts dev/bots/test.dart + matrix: + - name: tests_widgets-macos + env: + GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] + SHARD: tests + SUBSHARD: widgets + test_all_script: + - ulimit -S -n 2048 # https://github.com/flutter/flutter/issues/2976 + - dart --enable-asserts dev/bots/test.dart + - name: tests_framework_other-macos + env: + GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] + SHARD: tests + SUBSHARD: framework_other + test_all_script: + - ulimit -S -n 2048 # https://github.com/flutter/flutter/issues/2976 + - dart --enable-asserts dev/bots/test.dart + - name: tests_extras-macos + env: + GCLOUD_SERVICE_ACCOUNT_KEY: ENCRYPTED[f12abe60f5045d619ef4c79b83dd1e0722a0b0b13dbea95fbe334e2db7fffbcd841a5a92da8824848b539a19afe0c9fb] + SHARD: tests + SUBSHARD: extras + DEPOT_TOOLS: "tmp/depot_tools" + GOLDCTL: "$DEPOT_TOOLS/goldctl" + GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095] + goldctl_script: ./dev/bots/download_goldctl.sh + test_all_script: + - ulimit -S -n 2048 # https://github.com/flutter/flutter/issues/2976 + - dart --enable-asserts dev/bots/test.dart + - name: build_tests-macos + env: + SHARD: build_tests # we should also enable integration_tests at some point, but it's too flaky right now + COCOAPODS_DISABLE_STATS: true + FLUTTER_FRAMEWORK_DIR: "/tmp/flutter sdk/bin/cache/artifacts/engine/ios/" + osx_instance: + image: mojave-flutter + remove_preinstalled_flutter_script: rm -rf $FLUTTER_HOME + test_all_script: + - ulimit -S -n 2048 # https://github.com/flutter/flutter/issues/2976 + - dart --enable-asserts dev/bots/test.dart + - name: add2app-macos + env: + SHARD: add2app_test + test_all_script: + - ulimit -S -n 2048 # https://github.com/flutter/flutter/issues/2976 + - dart --enable-asserts dev/bots/test.dart + - name: customer_testing-macos + test_script: + - rm -rf bin/cache/pkg/tests + - git clone https://github.com/flutter/tests.git bin/cache/pkg/tests + - dart --enable-asserts dev/customer_testing/run_tests.dart --skip-on-fetch-failure --skip-template bin/cache/pkg/tests/registry/*.test docker_builder: # Only build a new docker image when we tag a release (for dev, beta, or release.) diff --git a/bin/internal/engine.version b/bin/internal/engine.version index 18b2cd15c1e..fe51880e83e 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -e695a516f148cef9c7850a6e00151f00bfadf0d4 +d1dcd1848633c5764e23313823445cbcba451a59 diff --git a/bin/internal/goldens.version b/bin/internal/goldens.version index d264de3f438..3299376f669 100644 --- a/bin/internal/goldens.version +++ b/bin/internal/goldens.version @@ -1 +1,2 @@ -b2cef0060add1b33bf65c97aaf80146b54cf7b86 +09a19be7d0cd25eb7602b6ead198e8cf234a9ed2 + diff --git a/dev/automated_tests/pubspec.yaml b/dev/automated_tests/pubspec.yaml index e663cf8c24b..12a9096f7d4 100644 --- a/dev/automated_tests/pubspec.yaml +++ b/dev/automated_tests/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" html: 0.14.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -59,10 +59,10 @@ dependencies: vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 6e23 +# PUBSPEC CHECKSUM: 4625 diff --git a/dev/benchmarks/complex_layout/pubspec.yaml b/dev/benchmarks/complex_layout/pubspec.yaml index 865f34a6195..c70f16dfe9e 100644 --- a/dev/benchmarks/complex_layout/pubspec.yaml +++ b/dev/benchmarks/complex_layout/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -45,7 +45,7 @@ dev_dependencies: analyzer: 0.36.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 1.5.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" html: 0.14.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -82,4 +82,4 @@ flutter: - packages/flutter_gallery_assets/people/square/ali.png - packages/flutter_gallery_assets/places/india_chettinad_silk_maker.png -# PUBSPEC CHECKSUM: 3eb2 +# PUBSPEC CHECKSUM: 86b4 diff --git a/dev/benchmarks/macrobenchmarks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/dev/benchmarks/macrobenchmarks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 3d43d11e66f..b6a5d3f48e4 100644 Binary files a/dev/benchmarks/macrobenchmarks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/dev/benchmarks/macrobenchmarks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/dev/benchmarks/macrobenchmarks/pubspec.yaml b/dev/benchmarks/macrobenchmarks/pubspec.yaml index 842f7cc6031..e5d0613357b 100644 --- a/dev/benchmarks/macrobenchmarks/pubspec.yaml +++ b/dev/benchmarks/macrobenchmarks/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -45,7 +45,7 @@ dev_dependencies: analyzer: 0.36.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 1.5.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" html: 0.14.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -79,4 +79,4 @@ dev_dependencies: flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 3eb2 +# PUBSPEC CHECKSUM: 86b4 diff --git a/dev/benchmarks/microbenchmarks/pubspec.yaml b/dev/benchmarks/microbenchmarks/pubspec.yaml index e8c6564675c..42e71f6c755 100644 --- a/dev/benchmarks/microbenchmarks/pubspec.yaml +++ b/dev/benchmarks/microbenchmarks/pubspec.yaml @@ -23,8 +23,8 @@ dependencies: collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - dart_style: 1.2.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + dart_style: 1.2.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" html: 0.14.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -67,10 +67,10 @@ dependencies: vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 1f87 +# PUBSPEC CHECKSUM: e88a diff --git a/dev/bots/download_goldctl.ps1 b/dev/bots/download_goldctl.ps1 new file mode 100644 index 00000000000..b983539987c --- /dev/null +++ b/dev/bots/download_goldctl.ps1 @@ -0,0 +1,5 @@ +$url= "https://chrome-infra-packages.appspot.com/p/skia/tools/goldctl/windows-amd64/+/" +$path = "c:\Windows\Temp\goldctl.zip" + +(New-Object System.Net.WebClient).DownloadFile($path, $output) +Expand-Archive -LiteralPath $path -DestinationPath "C:\Windows\Temp\goldctl_tool" diff --git a/dev/bots/download_goldctl.sh b/dev/bots/download_goldctl.sh new file mode 100755 index 00000000000..9ef81cc3784 --- /dev/null +++ b/dev/bots/download_goldctl.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +git clone --depth 1 https://chromium.googlesource.com/chromium/tools/depot_tools.git ./tmp/depot_tools +cd tmp/depot_tools +echo -e '# Ensure File\n$ServiceURL https://chrome-infra-packages.appspot.com\n\n# Skia Gold Client goldctl\nskia/tools/goldctl/${platform} latest' > ensure.txt +./cipd ensure -ensure-file ensure.txt -root . diff --git a/dev/bots/pubspec.yaml b/dev/bots/pubspec.yaml index 35d0193d074..152eaf0d536 100644 --- a/dev/bots/pubspec.yaml +++ b/dev/bots/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: http: 0.12.0+2 http_parser: 3.1.3 test: 1.6.3 - googleapis: 0.53.0 + googleapis: 0.54.0 googleapis_auth: 0.2.8 _discoveryapis_commons: 0.1.8+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -25,7 +25,7 @@ dependencies: collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 5.0.8+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -60,11 +60,11 @@ dependencies: typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: mockito: 4.1.0 test_api: 0.2.5 -# PUBSPEC CHECKSUM: ca5a +# PUBSPEC CHECKSUM: 245d diff --git a/dev/bots/run_command.dart b/dev/bots/run_command.dart index c97a08136dc..507ce595943 100644 --- a/dev/bots/run_command.dart +++ b/dev/bots/run_command.dart @@ -87,6 +87,7 @@ Future runCommand(String executable, List arguments, { bool skip = false, bool expectFlaky = false, Duration timeout = _kLongTimeout, + bool Function(String) removeLine, }) async { final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}'; final String relativeWorkingDir = path.relative(workingDirectory); @@ -103,13 +104,19 @@ Future runCommand(String executable, List arguments, { ); Future>> savedStdout, savedStderr; + final Stream> stdoutSource = process.stdout + .transform(const Utf8Decoder()) + .transform(const LineSplitter()) + .where((String line) => removeLine == null || !removeLine(line)) + .map((String line) => '$line\n') + .transform(const Utf8Encoder()); if (printOutput) { await Future.wait(>[ - stdout.addStream(process.stdout), + stdout.addStream(stdoutSource), stderr.addStream(process.stderr), ]); } else { - savedStdout = process.stdout.toList(); + savedStdout = stdoutSource.toList(); savedStderr = process.stderr.toList(); } diff --git a/dev/bots/test.dart b/dev/bots/test.dart index 7cb5d2df9bc..9bdbef1b9ce 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -240,7 +240,8 @@ Future _runToolTests() async { File(path.join(flutterRoot, 'bin', 'cache', 'flutter_tools.snapshot')).deleteSync(); File(path.join(flutterRoot, 'bin', 'cache', 'flutter_tools.stamp')).deleteSync(); } - if (noUseBuildRunner) { + // reduce overhead of build_runner in the create case. + if (noUseBuildRunner || Platform.environment['SUBSHARD'] == 'create') { await _pubRunTest( path.join(flutterRoot, 'packages', 'flutter_tools'), tableData: bigqueryApi?.tabledata, @@ -258,22 +259,23 @@ Future _runToolTests() async { print('${bold}DONE: All tests successful.$reset'); } -/// Verifies that AOT, APK, and IPA (if on macOS) builds of some -/// examples apps finish without crashing. It does not actually +/// Verifies that AOT, APK, and IPA (if on macOS) builds the +/// examples apps without crashing. It does not actually /// launch the apps. That happens later in the devicelab. This is /// just a smoke-test. In particular, this will verify we can build /// when there are spaces in the path name for the Flutter SDK and /// target app. Future _runBuildTests() async { - final List paths = [ - path.join('examples', 'hello_world'), - path.join('examples', 'flutter_gallery'), - path.join('examples', 'flutter_view'), - ]; - for (String path in paths) { - await _flutterBuildAot(path); - await _flutterBuildApk(path); - await _flutterBuildIpa(path); + final Stream exampleDirectories = Directory(path.join(flutterRoot, 'examples')).list(); + await for (FileSystemEntity fileEntity in exampleDirectories) { + if (fileEntity is! Directory) { + continue; + } + final String examplePath = fileEntity.path; + + await _flutterBuildAot(examplePath); + await _flutterBuildApk(examplePath); + await _flutterBuildIpa(examplePath); } await _flutterBuildDart2js(path.join('dev', 'integration_tests', 'web')); @@ -552,6 +554,7 @@ Future _buildRunnerTest( args, workingDirectory:workingDirectory, environment:pubEnvironment, + removeLine: (String line) => line.contains('[INFO]') ); } } @@ -588,6 +591,9 @@ Future _pubRunTest( case 'tool': args.addAll(['--exclude-tags', 'integration']); break; + case 'create': + args.addAll([path.join('test', 'commands', 'create_test.dart')]); + break; } if (useFlutterTestFormatter) { @@ -603,7 +609,7 @@ Future _pubRunTest( await runCommand( pub, args, - workingDirectory:workingDirectory, + workingDirectory: workingDirectory, ); } } diff --git a/dev/customer_testing/pubspec.yaml b/dev/customer_testing/pubspec.yaml new file mode 100644 index 00000000000..5fd49f885f4 --- /dev/null +++ b/dev/customer_testing/pubspec.yaml @@ -0,0 +1,20 @@ +name: customer_testing +description: Tool to run the tests listed in the flutter/tests repository. + +environment: + sdk: any + +dependencies: + args: 1.5.2 + path: 1.6.2 + glob: 1.1.7 + meta: 1.1.6 + + async: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + charcode: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_span: 1.5.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + string_scanner: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + term_glyph: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +# PUBSPEC CHECKSUM: f971 diff --git a/dev/customer_testing/run_tests.dart b/dev/customer_testing/run_tests.dart new file mode 100644 index 00000000000..9d0b62ea7d3 --- /dev/null +++ b/dev/customer_testing/run_tests.dart @@ -0,0 +1,263 @@ +// Copyright 2017 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:convert'; +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:glob/glob.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; + +Future main(List arguments) async { + exit(await run(arguments) ? 0 : 1); +} + +Future run(List arguments) async { + final ArgParser argParser = ArgParser( + allowTrailingOptions: false, + usageLineLength: 72, + ) + ..addOption( + 'repeat', + defaultsTo: '1', + help: 'How many times to run each test. Set to a high value to look for flakes.', + valueHelp: 'count', + ) + ..addFlag( + 'skip-on-fetch-failure', + defaultsTo: false, + help: 'Whether to skip tests that we fail to download.', + ) + ..addFlag( + 'skip-template', + defaultsTo: false, + help: 'Whether to skip tests named "template.test".', + ) + ..addFlag( + 'verbose', + defaultsTo: false, + help: 'Describe what is happening in detail.', + ) + ..addFlag( + 'help', + defaultsTo: false, + negatable: false, + help: 'Print this help message.', + ); + + final ArgResults parsedArguments = argParser.parse(arguments); + + final int repeat = int.tryParse(parsedArguments['repeat']); + final bool skipOnFetchFailure = parsedArguments['skip-on-fetch-failure']; + final bool skipTemplate = parsedArguments['skip-template']; + final bool verbose = parsedArguments['verbose']; + final bool help = parsedArguments['help']; + final List files = parsedArguments + .rest + .expand((String path) => Glob(path).listSync()) + .whereType() + .where((File file) => !skipTemplate || path.basename(file.path) != 'template.test') + .toList(); + + if (help || repeat == null || files.isEmpty) { + print('run_tests.dart [options...] path/to/file1.test path/to/file2.test...'); + print('For details on the test registry format, see:'); + print(' https://github.com/flutter/tests/blob/master/registry/template.test'); + print(''); + print(argParser.usage); + print(''); + return help; + } + + if (verbose) + print('Starting run_tests.dart...'); + + int failures = 0; + + if (verbose) { + final String s = files.length == 1 ? '' : 's'; + print('${files.length} file$s specified.'); + print(''); + } + + for (File file in files) { + if (verbose) + print('Processing ${file.path}...'); + TestFile instructions; + try { + instructions = TestFile(file); + } on FormatException catch (error) { + print('ERROR: ${error.message}'); + print(''); + failures += 1; + continue; + } on FileSystemException catch (error) { + print('ERROR: ${error.message}'); + print(' ${file.path}'); + print(''); + failures += 1; + continue; + } + + final Directory checkout = Directory.systemTemp.createTempSync('flutter_customer_testing.${path.basenameWithoutExtension(file.path)}.'); + if (verbose) + print('Created temporary directory: ${checkout.path}'); + try { + bool success; + bool showContacts = false; + for (String fetchCommand in instructions.fetch) { + success = await shell(fetchCommand, checkout, verbose: verbose, silentFailure: skipOnFetchFailure); + if (!success) { + if (skipOnFetchFailure) { + if (verbose) { + print('Skipping (fetch failed).'); + } else { + print('Skipping ${file.path} (fetch failed).'); + } + } else { + print('ERROR: Failed to fetch repository.'); + failures += 1; + showContacts = true; + } + break; + } + } + assert(success != null); + if (success) { + if (verbose) + print('Running tests...'); + final Directory tests = Directory(path.join(checkout.path, 'tests')); + // TODO(ianh): Once we have a way to update source code, run that command in each directory of instructions.update + for (int iteration = 0; iteration < repeat; iteration += 1) { + if (verbose && repeat > 1) + print('Round ${iteration + 1} of $repeat.'); + for (String testCommand in instructions.tests) { + success = await shell(testCommand, tests, verbose: verbose); + if (!success) { + print('ERROR: One or more tests from ${path.basenameWithoutExtension(file.path)} failed.'); + failures += 1; + showContacts = true; + break; + } + } + } + if (verbose && success) + print('Tests finished.'); + } + if (showContacts) { + final String s = instructions.contacts.length == 1 ? '' : 's'; + print('Contact$s: ${instructions.contacts.join(", ")}'); + } + } finally { + if (verbose) + print('Deleting temporary directory...'); + checkout.deleteSync(recursive: true); + } + if (verbose) + print(''); + } + if (failures > 0) { + final String s = failures == 1 ? '' : 's'; + print('$failures failure$s.'); + return false; + } + if (verbose) { + print('All tests passed!'); + } + return true; +} + +@immutable +class TestFile { + factory TestFile(File file) { + final String errorPrefix = 'Could not parse: ${file.path}\n'; + final List contacts = []; + final List fetch = []; + final List update = []; + final List test = []; + for (String line in file.readAsLinesSync().map((String line) => line.trim())) { + if (line.isEmpty) { + // blank line + } else if (line.startsWith('#')) { + // comment + } else if (line.startsWith('contact=')) { + contacts.add(line.substring(8)); + } else if (line.startsWith('fetch=')) { + fetch.add(line.substring(6)); + } else if (line.startsWith('update=')) { + update.add(Directory(line.substring(7))); + } else if (line.startsWith('test=')) { + test.add(line.substring(5)); + } else { + throw FormatException('${errorPrefix}Unexpected directive:\n$line'); + } + } + if (contacts.isEmpty) + throw FormatException('${errorPrefix}No contacts specified. At least one contact e-mail address must be specified.'); + for (String email in contacts) { + if (!email.contains(_email) || email.endsWith('@example.com')) + throw FormatException('${errorPrefix}The following e-mail address appears to be an invalid e-mail address: $email'); + } + if (fetch.isEmpty) + throw FormatException('${errorPrefix}No "fetch" directives specified. Two lines are expected: "git clone https://github.com/USERNAME/REPOSITORY.git tests" and "git -C tests checkout HASH".'); + if (fetch.length < 2) + throw FormatException('${errorPrefix}Only one "fetch" directive specified. Two lines are expected: "git clone https://github.com/USERNAME/REPOSITORY.git tests" and "git -C tests checkout HASH".'); + if (!fetch[0].contains(_fetch1)) + throw FormatException('${errorPrefix}First "fetch" directive does not match expected pattern (expected "git clone https://github.com/USERNAME/REPOSITORY.git tests").'); + if (!fetch[1].contains(_fetch2)) + throw FormatException('${errorPrefix}Second "fetch" directive does not match expected pattern (expected "git -C tests checkout HASH").'); + if (update.isEmpty) + throw FormatException('${errorPrefix}No "update" directives specified. At least one directory must be specified. (It can be "." to just upgrade the root of the repository.)'); + if (test.isEmpty) + throw FormatException('${errorPrefix}No "test" directives specified. At least one command must be specified to run tests.'); + return TestFile._( + List.unmodifiable(contacts), + List.unmodifiable(fetch), + List.unmodifiable(update), + List.unmodifiable(test), + ); + } + + const TestFile._(this.contacts, this.fetch, this.update, this.tests); + + // (e-mail regexp from HTML standard) + static final RegExp _email = RegExp(r'''^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$'''); + static final RegExp _fetch1 = RegExp(r'^git clone https://github.com/[-a-zA-Z0-9]+/[-_a-zA-Z0-9]+.git tests$'); + static final RegExp _fetch2 = RegExp(r'^git -C tests checkout [0-9a-f]+$'); + + final List contacts; + final List fetch; + final List update; + final List tests; +} + +final RegExp _spaces = RegExp(r' +'); + +Future shell(String command, Directory directory, { bool verbose = false, bool silentFailure = false }) async { + if (verbose) + print('>> $command'); + Process process; + if (Platform.isWindows) { + process = await Process.start('CMD.EXE', ['/S', '/C', '$command'], workingDirectory: directory.path); + } else { + final List segments = command.trim().split(_spaces); + process = await Process.start(segments.first, segments.skip(1).toList(), workingDirectory: directory.path); + } + final List output = []; + utf8.decoder.bind(process.stdout).transform(const LineSplitter()).listen(verbose ? printLog : output.add); + utf8.decoder.bind(process.stderr).transform(const LineSplitter()).listen(verbose ? printLog : output.add); + final bool success = await process.exitCode == 0; + if (success || silentFailure) + return success; + if (!verbose) { + print('>> $command'); + output.forEach(printLog); + } + return success; +} + +void printLog(String line) { + print('| $line'.trimRight()); +} diff --git a/dev/devicelab/bin/tasks/platform_channel_sample_test_swift.dart b/dev/devicelab/bin/tasks/platform_channel_sample_test_swift.dart new file mode 100644 index 00000000000..18a1d59c921 --- /dev/null +++ b/dev/devicelab/bin/tasks/platform_channel_sample_test_swift.dart @@ -0,0 +1,14 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/adb.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/integration_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + await task(createPlatformChannelSwiftSampleTest()); +} diff --git a/dev/devicelab/images/agent-statuses.png b/dev/devicelab/images/agent-statuses.png index d0b3dc5fb32..ad5eba6a2bc 100644 Binary files a/dev/devicelab/images/agent-statuses.png and b/dev/devicelab/images/agent-statuses.png differ diff --git a/dev/devicelab/images/broken-test.png b/dev/devicelab/images/broken-test.png index cf6db90a50a..fbde6294d24 100644 Binary files a/dev/devicelab/images/broken-test.png and b/dev/devicelab/images/broken-test.png differ diff --git a/dev/devicelab/images/legend.png b/dev/devicelab/images/legend.png index c9bd9a05952..cb94696556d 100644 Binary files a/dev/devicelab/images/legend.png and b/dev/devicelab/images/legend.png differ diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart index 116403eb94f..a940a24c8ac 100644 --- a/dev/devicelab/lib/tasks/integration_tests.dart +++ b/dev/devicelab/lib/tasks/integration_tests.dart @@ -47,6 +47,13 @@ TaskFunction createPlatformChannelSampleTest() { ); } +TaskFunction createPlatformChannelSwiftSampleTest() { + return DriverTest( + '${flutterDirectory.path}/examples/platform_channel_swift', + 'test_driver/button_tap.dart', + ); +} + TaskFunction createEmbeddedAndroidViewsIntegrationTest() { return DriverTest( '${flutterDirectory.path}/dev/integration_tests/android_views', diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml index 579508ae92c..5cafc980263 100644 --- a/dev/devicelab/manifest.yaml +++ b/dev/devicelab/manifest.yaml @@ -414,10 +414,17 @@ tasks: platform_channel_sample_test_ios: description: > - Runs a driver test on the Platform Channel sample app on iPhone 6. + Runs a driver test on the Platform Channel sample app on iPhone 6 Objective-C project. stage: devicelab_ios required_agent_capabilities: ["mac/ios"] + platform_channel_sample_test_swift: + description: > + Runs a driver test on the Platform Channel sample app on iPhone 6 Swift project. + stage: devicelab_ios + required_agent_capabilities: ["mac/ios"] + flaky: true + platform_view_ios__start_up: description: > Verifies that Platform View can be used from an iOS project. diff --git a/dev/devicelab/pubspec.yaml b/dev/devicelab/pubspec.yaml index cd7193f3a44..739df888522 100644 --- a/dev/devicelab/pubspec.yaml +++ b/dev/devicelab/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: stream_channel: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" xml: 3.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: @@ -43,7 +43,7 @@ dev_dependencies: analyzer: 0.36.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" html: 0.14.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -73,4 +73,4 @@ dev_dependencies: watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 9ffc +# PUBSPEC CHECKSUM: 2cfe diff --git a/dev/docs/assets/snippets.css b/dev/docs/assets/snippets.css index 4fb200addbc..e5ee48f170b 100644 --- a/dev/docs/assets/snippets.css +++ b/dev/docs/assets/snippets.css @@ -83,6 +83,26 @@ font-family: courier, lucidia; } +.anchor-container { + position: relative; +} + +.anchor-button-overlay { + position: absolute; + top: 0px; + right: 5px; + height: 28px; + width: 28px; + transition: .3s ease; + background-color: #2372a3; +} + +.anchor-button { + border-style: none; + background: none; + cursor: pointer; +} + /* Styles for the copy-to-clipboard button */ .copyable-container { position: relative; diff --git a/dev/docs/assets/snippets.js b/dev/docs/assets/snippets.js index 9d4da921dac..86e9a2c1174 100644 --- a/dev/docs/assets/snippets.js +++ b/dev/docs/assets/snippets.js @@ -57,6 +57,30 @@ function supportsCopying() { !!document.queryCommandSupported('copy'); } +// Copies the given string to the clipboard. +function copyStringToClipboard(string) { + var textArea = document.createElement("textarea"); + textArea.value = string; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + if (!supportsCopying()) { + alert('Unable to copy to clipboard (not supported by browser)'); + return; + } + + try { + document.execCommand('copy'); + } finally { + document.body.removeChild(textArea); + } +} + +function fixHref(anchor, id) { + anchor.href = window.location.href.replace(/#.*$/, '') + '#' + id; +} + // Copies the text inside the currently visible snippet to the clipboard, or the // given element, if any. function copyTextToClipboard(element) { diff --git a/dev/integration_tests/android_semantics_testing/pubspec.yaml b/dev/integration_tests/android_semantics_testing/pubspec.yaml index 9f9b97dec74..c816b7653d9 100644 --- a/dev/integration_tests/android_semantics_testing/pubspec.yaml +++ b/dev/integration_tests/android_semantics_testing/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 5.0.8+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -57,10 +57,10 @@ dependencies: vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 42c8 +# PUBSPEC CHECKSUM: 32ca diff --git a/dev/integration_tests/android_views/pubspec.yaml b/dev/integration_tests/android_views/pubspec.yaml index 5339b245a52..cf862fe9197 100644 --- a/dev/integration_tests/android_views/pubspec.yaml +++ b/dev/integration_tests/android_views/pubspec.yaml @@ -7,7 +7,7 @@ dependencies: sdk: flutter flutter_driver: sdk: flutter - path_provider: 1.1.0 + path_provider: 1.1.2 collection: 1.14.11 assets_for_android_views: git: @@ -32,7 +32,7 @@ dependencies: typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -42,7 +42,7 @@ dev_dependencies: analyzer: 0.36.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 1.5.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" html: 0.14.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -76,4 +76,4 @@ dev_dependencies: flutter: uses-material-design: true -# PUBSPEC CHECKSUM: dd79 +# PUBSPEC CHECKSUM: 017d diff --git a/dev/integration_tests/channels/pubspec.yaml b/dev/integration_tests/channels/pubspec.yaml index 57c44de7615..d890ca60e66 100644 --- a/dev/integration_tests/channels/pubspec.yaml +++ b/dev/integration_tests/channels/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 5.0.8+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -61,10 +61,10 @@ dependencies: vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 42c8 +# PUBSPEC CHECKSUM: 32ca diff --git a/dev/integration_tests/codegen/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/dev/integration_tests/codegen/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 3d43d11e66f..b6a5d3f48e4 100644 Binary files a/dev/integration_tests/codegen/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/dev/integration_tests/codegen/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/dev/integration_tests/codegen/pubspec.yaml b/dev/integration_tests/codegen/pubspec.yaml index 1768fec2a6b..1f28fea8d00 100644 --- a/dev/integration_tests/codegen/pubspec.yaml +++ b/dev/integration_tests/codegen/pubspec.yaml @@ -29,7 +29,7 @@ dependencies: typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: test: 1.6.3 @@ -37,7 +37,7 @@ dev_dependencies: analyzer: 0.36.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 1.5.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" html: 0.14.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -74,4 +74,4 @@ builders: flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 42c8 +# PUBSPEC CHECKSUM: 32ca diff --git a/dev/integration_tests/external_ui/pubspec.yaml b/dev/integration_tests/external_ui/pubspec.yaml index 032ecb02629..c5d54a431e2 100644 --- a/dev/integration_tests/external_ui/pubspec.yaml +++ b/dev/integration_tests/external_ui/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 5.0.8+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -61,10 +61,10 @@ dependencies: vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 42c8 +# PUBSPEC CHECKSUM: 32ca diff --git a/dev/integration_tests/flavors/pubspec.yaml b/dev/integration_tests/flavors/pubspec.yaml index b20cd9fef62..44221897c0b 100644 --- a/dev/integration_tests/flavors/pubspec.yaml +++ b/dev/integration_tests/flavors/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 5.0.8+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -61,10 +61,10 @@ dependencies: vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 42c8 +# PUBSPEC CHECKSUM: 32ca diff --git a/dev/integration_tests/image_loading/pubspec.yaml b/dev/integration_tests/image_loading/pubspec.yaml index fe83bd39a05..1e5e0c108ef 100644 --- a/dev/integration_tests/image_loading/pubspec.yaml +++ b/dev/integration_tests/image_loading/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" html: 0.14.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -54,10 +54,10 @@ dependencies: vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 967d +# PUBSPEC CHECKSUM: 707f diff --git a/dev/integration_tests/ios_add2app/ios_add2app/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/dev/integration_tests/ios_add2app/ios_add2app/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 3d43d11e66f..b6a5d3f48e4 100644 Binary files a/dev/integration_tests/ios_add2app/ios_add2app/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/dev/integration_tests/ios_add2app/ios_add2app/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/dev/integration_tests/platform_interaction/pubspec.yaml b/dev/integration_tests/platform_interaction/pubspec.yaml index b501a43f20b..e4784f50cb6 100644 --- a/dev/integration_tests/platform_interaction/pubspec.yaml +++ b/dev/integration_tests/platform_interaction/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 5.0.8+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -61,10 +61,10 @@ dependencies: vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 42c8 +# PUBSPEC CHECKSUM: 32ca diff --git a/dev/integration_tests/release_smoke_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/dev/integration_tests/release_smoke_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 3d43d11e66f..b6a5d3f48e4 100644 Binary files a/dev/integration_tests/release_smoke_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/dev/integration_tests/release_smoke_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/dev/integration_tests/simple_codegen/pubspec.yaml b/dev/integration_tests/simple_codegen/pubspec.yaml index 5249293102f..dfaeabc9761 100644 --- a/dev/integration_tests/simple_codegen/pubspec.yaml +++ b/dev/integration_tests/simple_codegen/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" html: 0.14.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -32,4 +32,4 @@ environment: # The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite. sdk: ">=2.0.0-dev.68.0 <3.0.0" -# PUBSPEC CHECKSUM: db9f +# PUBSPEC CHECKSUM: b4a0 diff --git a/dev/integration_tests/ui/pubspec.yaml b/dev/integration_tests/ui/pubspec.yaml index 0b9ab4606b2..8010d9eb5f7 100644 --- a/dev/integration_tests/ui/pubspec.yaml +++ b/dev/integration_tests/ui/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 5.0.8+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -63,7 +63,7 @@ dependencies: vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" xml: 3.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -77,4 +77,4 @@ dev_dependencies: flutter: uses-material-design: true -# PUBSPEC CHECKSUM: ce0d +# PUBSPEC CHECKSUM: 3d0f diff --git a/dev/snippets/assets/code_sample.png b/dev/snippets/assets/code_sample.png index 3a9b91debff..cc3e125cc5e 100644 Binary files a/dev/snippets/assets/code_sample.png and b/dev/snippets/assets/code_sample.png differ diff --git a/dev/snippets/assets/code_snippet.png b/dev/snippets/assets/code_snippet.png index 8fb51b22497..96bf7e86bfb 100644 Binary files a/dev/snippets/assets/code_snippet.png and b/dev/snippets/assets/code_snippet.png differ diff --git a/dev/snippets/config/skeletons/application.html b/dev/snippets/config/skeletons/application.html index 7d7e17bab47..56b648cc033 100644 --- a/dev/snippets/config/skeletons/application.html +++ b/dev/snippets/config/skeletons/application.html @@ -1,4 +1,13 @@ {@inject-html} + +
diff --git a/dev/snippets/lib/main.dart b/dev/snippets/lib/main.dart index 786833d161b..996b186c9bb 100644 --- a/dev/snippets/lib/main.dart +++ b/dev/snippets/lib/main.dart @@ -152,13 +152,13 @@ void main(List argList) { input, snippetType, template: template, - id: id.join('.'), output: args[_kOutputOption] != null ? File(args[_kOutputOption]) : null, metadata: { 'sourcePath': environment['SOURCE_PATH'], 'sourceLine': environment['SOURCE_LINE'] != null ? int.tryParse(environment['SOURCE_LINE']) : null, + 'id': id.join('.'), 'serial': serial, 'package': packageName, 'library': libraryName, diff --git a/dev/snippets/lib/snippets.dart b/dev/snippets/lib/snippets.dart index 9f69daec2a2..05d54f4c169 100644 --- a/dev/snippets/lib/snippets.dart +++ b/dev/snippets/lib/snippets.dart @@ -5,8 +5,9 @@ import 'dart:convert'; import 'dart:io'; -import 'package:path/path.dart' as path; import 'package:dart_style/dart_style.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; import 'configuration.dart'; @@ -128,13 +129,12 @@ class SnippetGenerator { 'code': htmlEscape.convert(result.join('\n')), 'language': language ?? 'dart', 'serial': '', - 'id': '', + 'id': metadata['id'], 'app': '', }; if (type == SnippetType.application) { substitutions - ..['serial'] = metadata['serial'].toString() ?? '0' - ..['id'] = injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'id').mergedContent + ..['serial'] = metadata['serial']?.toString() ?? '0' ..['app'] = htmlEscape.convert(injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'app').mergedContent); } return skeleton.replaceAllMapped(RegExp('{{(${substitutions.keys.join('|')})}}'), (Match match) { @@ -209,9 +209,15 @@ class SnippetGenerator { /// The [id] is a string ID to use for the output file, and to tell the user /// about in the `flutter create` hint. It must not be null if the [type] is /// [SnippetType.application]. - String generate(File input, SnippetType type, {String template, String id, File output, Map metadata}) { + String generate( + File input, + SnippetType type, { + String template, + File output, + @required Map metadata, + }) { assert(template != null || type != SnippetType.application); - assert(id != null || type != SnippetType.application); + assert(metadata != null && metadata['id'] != null); assert(input != null); final List<_ComponentTuple> snippetData = parseInput(_loadFileAsUtf8(input)); switch (type) { @@ -227,7 +233,6 @@ class SnippetGenerator { 'The template $template was not found in the templates directory ${templatesDir.path}'); exit(1); } - snippetData.add(_ComponentTuple('id', [id])); final String templateContents = _loadFileAsUtf8(templateFile); String app = interpolateTemplate(snippetData, templateContents); @@ -239,7 +244,7 @@ class SnippetGenerator { } snippetData.add(_ComponentTuple('app', app.split('\n'))); - final File outputFile = output ?? getOutputFile(id); + final File outputFile = output ?? getOutputFile(metadata['id']); stderr.writeln('Writing to ${outputFile.absolute.path}'); outputFile.writeAsStringSync(app); @@ -252,7 +257,7 @@ class SnippetGenerator { ); metadata ??= {}; metadata.addAll({ - 'id': id, + 'id': metadata['id'], 'file': path.basename(outputFile.path), 'description': description?.mergedContent, }); diff --git a/dev/snippets/pubspec.yaml b/dev/snippets/pubspec.yaml index b7127ea2603..38487b51653 100644 --- a/dev/snippets/pubspec.yaml +++ b/dev/snippets/pubspec.yaml @@ -14,7 +14,7 @@ dartdoc: dependencies: args: 1.5.2 - dart_style: 1.2.8 + dart_style: 1.2.9 meta: 1.1.6 platform: 2.2.0 @@ -24,7 +24,7 @@ dependencies: collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" html: 0.14.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -67,7 +67,7 @@ dev_dependencies: test_api: 0.2.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" test_core: 0.2.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" executables: snippets: null @@ -98,4 +98,4 @@ executables: vm_service_client: 0.2.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 1.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 198b +# PUBSPEC CHECKSUM: 918e diff --git a/dev/snippets/test/snippets_test.dart b/dev/snippets/test/snippets_test.dart index 6408623cac4..f3cb758a3cc 100644 --- a/dev/snippets/test/snippets_test.dart +++ b/dev/snippets/test/snippets_test.dart @@ -74,8 +74,14 @@ void main() { ``` '''); - final String html = - generator.generate(inputFile, SnippetType.application, template: 'template', id: 'id'); + final String html = generator.generate( + inputFile, + SnippetType.application, + template: 'template', + metadata: { + 'id': 'id', + }, + ); expect(html, contains('
HTML Bits
')); expect(html, contains('
More HTML Bits
')); expect(html, contains('print('The actual \$name.');')); @@ -103,7 +109,7 @@ void main() { ``` '''); - final String html = generator.generate(inputFile, SnippetType.sample); + final String html = generator.generate(inputFile, SnippetType.sample, metadata: {'id': 'id'}); expect(html, contains('
HTML Bits
')); expect(html, contains('
More HTML Bits
')); expect(html, contains(' print('The actual \$name.');')); @@ -135,9 +141,8 @@ void main() { inputFile, SnippetType.application, template: 'template', - id: 'id', output: outputFile, - metadata: {'sourcePath': 'some/path.dart'}, + metadata: {'sourcePath': 'some/path.dart', 'id': 'id'}, ); expect(expectedMetadataFile.existsSync(), isTrue); final Map json = jsonDecode(expectedMetadataFile.readAsStringSync()); diff --git a/examples/catalog/pubspec.yaml b/examples/catalog/pubspec.yaml index e38ba0ef3ee..0a92756dcde 100644 --- a/examples/catalog/pubspec.yaml +++ b/examples/catalog/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: charcode: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 5.0.8+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -67,10 +67,10 @@ dev_dependencies: test_core: 0.2.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: a7b3 +# PUBSPEC CHECKSUM: b1b5 diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_background.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_background.png index c5ccd079eff..651fbd04c30 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_background.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_background.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_foreground.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_foreground.png index e3c6fb0d528..373133cbbca 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_foreground.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_foreground.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index b8f67f1933d..41ebbcad375 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_background.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_background.png index d05e1b6799b..e322899c925 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_background.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_background.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png index 052b9828671..45eb7427856 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 6b188646750..0f6226d7f83 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_background.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_background.png index 455f042a62e..e40d603baca 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_background.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_background.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png index fa40533725a..87d69a71557 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index fcdb89bade5..eaba4179b78 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png index 3fd2f4fa22a..cd5aea93afb 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png index c5df8264954..615f0b8aad9 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 87050b46b09..c9ac0c3ca9b 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/examples/flutter_gallery/ios/Runner.xcodeproj/project.pbxproj b/examples/flutter_gallery/ios/Runner.xcodeproj/project.pbxproj index e2bd6ae9ac5..252987cabc0 100644 --- a/examples/flutter_gallery/ios/Runner.xcodeproj/project.pbxproj +++ b/examples/flutter_gallery/ios/Runner.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 26D36078B4738B64685A0B6F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 2D9222431EC1E1BA007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 2D9222441EC1E1BA007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -55,6 +56,8 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C77CA0BBC4B57129484236F4 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + DC01738FBE39EADD5AC4BF42 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + ECF490DDAB8ABCEEFB1780BE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -138,6 +141,9 @@ E54E8B7296D73DD9B2385312 /* Pods */ = { isa = PBXGroup; children = ( + ECF490DDAB8ABCEEFB1780BE /* Pods-Runner.debug.xcconfig */, + 26D36078B4738B64685A0B6F /* Pods-Runner.release.xcconfig */, + DC01738FBE39EADD5AC4BF42 /* Pods-Runner.profile.xcconfig */, ); name = Pods; sourceTree = ""; @@ -273,7 +279,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ diff --git a/examples/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-120.png b/examples/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-120.png index 4a6aeb333b2..6b4872a6ee7 100644 Binary files a/examples/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-120.png and b/examples/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-120.png differ diff --git a/examples/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-152.png b/examples/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-152.png index f00dddf63ba..58e4e750829 100644 Binary files a/examples/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-152.png and b/examples/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-152.png differ diff --git a/examples/flutter_gallery/pubspec.yaml b/examples/flutter_gallery/pubspec.yaml index 1e544e22ef9..31f9e280d39 100644 --- a/examples/flutter_gallery/pubspec.yaml +++ b/examples/flutter_gallery/pubspec.yaml @@ -10,9 +10,9 @@ dependencies: collection: 1.14.11 device_info: 0.4.0+2 intl: 0.15.8 - connectivity: 0.4.3+2 + connectivity: 0.4.3+4 string_scanner: 1.0.4 - url_launcher: 5.0.3 + url_launcher: 5.0.5 cupertino_icons: 0.1.2 video_player: 0.10.1+3 scoped_model: 1.0.1 @@ -44,7 +44,7 @@ dev_dependencies: boolean_selector: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 5.0.8+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -80,7 +80,7 @@ dev_dependencies: test_core: 0.2.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -254,4 +254,4 @@ flutter: - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Regular.ttf - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Light.ttf -# PUBSPEC CHECKSUM: bd7b +# PUBSPEC CHECKSUM: 7e81 diff --git a/examples/flutter_view/ios/Runner.xcodeproj/project.pbxproj b/examples/flutter_view/ios/Runner.xcodeproj/project.pbxproj index 42d6b8fca46..edc2bcc0688 100644 --- a/examples/flutter_view/ios/Runner.xcodeproj/project.pbxproj +++ b/examples/flutter_view/ios/Runner.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ 2DE332E81E55C6F100393FD5 /* MainViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MainViewController.h; sourceTree = ""; }; 3B3967011E83382E004F5970 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 3B3967041E83383D004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 63EC5EC13E843CD861057871 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -60,6 +61,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C50B4FE91C29B0DE9DD62DD3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + EADA814501F2EF49C9E6C636 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -79,6 +82,9 @@ 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { isa = PBXGroup; children = ( + 63EC5EC13E843CD861057871 /* Pods-Runner.debug.xcconfig */, + C50B4FE91C29B0DE9DD62DD3 /* Pods-Runner.release.xcconfig */, + EADA814501F2EF49C9E6C636 /* Pods-Runner.profile.xcconfig */, ); name = Pods; sourceTree = ""; @@ -165,7 +171,6 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, D7EBAA0AD2D4385BA6FA83BA /* [CP] Embed Pods Frameworks */, - 0273455D92E89802918C824F /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -226,21 +231,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 0273455D92E89802918C824F /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -275,13 +265,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; D7EBAA0AD2D4385BA6FA83BA /* [CP] Embed Pods Frameworks */ = { @@ -296,7 +289,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ diff --git a/examples/platform_channel/pubspec.yaml b/examples/platform_channel/pubspec.yaml index 1805d9ab8ef..45ecc282293 100644 --- a/examples/platform_channel/pubspec.yaml +++ b/examples/platform_channel/pubspec.yaml @@ -27,7 +27,7 @@ dev_dependencies: charcode: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 5.0.8+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -66,10 +66,10 @@ dev_dependencies: test_core: 0.2.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: a7b3 +# PUBSPEC CHECKSUM: b1b5 diff --git a/examples/platform_channel_swift/android/app/build.gradle b/examples/platform_channel_swift/android/app/build.gradle new file mode 100644 index 00000000000..50c275da632 --- /dev/null +++ b/examples/platform_channel_swift/android/app/build.gradle @@ -0,0 +1,60 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 28 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.examples.platform_channel_swift" + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} diff --git a/examples/platform_channel_swift/android/app/src/androidTest/java/com/example/platformchannel/ExampleInstrumentedTest.java b/examples/platform_channel_swift/android/app/src/androidTest/java/com/example/platformchannel/ExampleInstrumentedTest.java new file mode 100644 index 00000000000..8c1bf37ee2b --- /dev/null +++ b/examples/platform_channel_swift/android/app/src/androidTest/java/com/example/platformchannel/ExampleInstrumentedTest.java @@ -0,0 +1,94 @@ +package com.example.platformchannel; + +import android.graphics.Bitmap; +import android.support.test.InstrumentationRegistry; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import io.flutter.view.FlutterView; + +import android.app.Instrumentation; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Rule + public ActivityTestRule activityRule = + new ActivityTestRule<>(MainActivity.class); + + + @Test + public void testBitmap() { + final Instrumentation instr = InstrumentationRegistry.getInstrumentation(); + final BitmapPoller poller = new BitmapPoller(5); + instr.runOnMainSync(new Runnable() { + public void run() { + final FlutterView flutterView = activityRule.getActivity().getFlutterView(); + + // Call onPostResume to start the engine's renderer even if the activity + // is paused in the test environment. + flutterView.onPostResume(); + + poller.start(flutterView); + } + }); + + Bitmap bitmap = null; + try { + bitmap = poller.waitForBitmap(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + + assertNotNull(bitmap); + assertTrue(bitmap.getWidth() > 0); + assertTrue(bitmap.getHeight() > 0); + + // Check that a pixel matches the default Material background color. + assertTrue(bitmap.getPixel(bitmap.getWidth() - 1, bitmap.getHeight() - 1) == 0xFFFAFAFA); + } + + // Waits on a FlutterView until it is able to produce a bitmap. + private class BitmapPoller { + private int triesPending; + private int waitMsec; + private FlutterView flutterView; + private Bitmap bitmap; + private CountDownLatch latch = new CountDownLatch(1); + + private final int delayMsec = 1000; + + BitmapPoller(int tries) { + triesPending = tries; + waitMsec = delayMsec * tries + 100; + } + + void start(FlutterView flutterView) { + this.flutterView = flutterView; + flutterView.postDelayed(checkBitmap, delayMsec); + } + + Bitmap waitForBitmap() throws InterruptedException { + latch.await(waitMsec, TimeUnit.MILLISECONDS); + return bitmap; + } + + private Runnable checkBitmap = new Runnable() { + public void run() { + bitmap = flutterView.getBitmap(); + triesPending--; + if (bitmap != null || triesPending == 0) { + latch.countDown(); + } else { + flutterView.postDelayed(checkBitmap, delayMsec); + } + } + }; + } +} diff --git a/examples/platform_channel_swift/android/app/src/main/AndroidManifest.xml b/examples/platform_channel_swift/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..fe87a8a9dfe --- /dev/null +++ b/examples/platform_channel_swift/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + diff --git a/examples/platform_channel_swift/android/app/src/main/java/com/example/platformchannel/MainActivity.java b/examples/platform_channel_swift/android/app/src/main/java/com/example/platformchannel/MainActivity.java new file mode 100644 index 00000000000..19db0603723 --- /dev/null +++ b/examples/platform_channel_swift/android/app/src/main/java/com/example/platformchannel/MainActivity.java @@ -0,0 +1,101 @@ +// Copyright 2017 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. + +package com.example.platformchannel; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; + +import io.flutter.app.FlutterActivity; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.EventChannel.EventSink; +import io.flutter.plugin.common.EventChannel.StreamHandler; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class MainActivity extends FlutterActivity { + private static final String BATTERY_CHANNEL = "samples.flutter.io/battery"; + private static final String CHARGING_CHANNEL = "samples.flutter.io/charging"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + new EventChannel(getFlutterView(), CHARGING_CHANNEL).setStreamHandler( + new StreamHandler() { + private BroadcastReceiver chargingStateChangeReceiver; + @Override + public void onListen(Object arguments, EventSink events) { + chargingStateChangeReceiver = createChargingStateChangeReceiver(events); + registerReceiver( + chargingStateChangeReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + } + + @Override + public void onCancel(Object arguments) { + unregisterReceiver(chargingStateChangeReceiver); + chargingStateChangeReceiver = null; + } + } + ); + + new MethodChannel(getFlutterView(), BATTERY_CHANNEL).setMethodCallHandler( + new MethodCallHandler() { + @Override + public void onMethodCall(MethodCall call, Result result) { + if (call.method.equals("getBatteryLevel")) { + int batteryLevel = getBatteryLevel(); + + if (batteryLevel != -1) { + result.success(batteryLevel); + } else { + result.error("UNAVAILABLE", "Battery level not available.", null); + } + } else { + result.notImplemented(); + } + } + } + ); + } + + private BroadcastReceiver createChargingStateChangeReceiver(final EventSink events) { + return new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + + if (status == BatteryManager.BATTERY_STATUS_UNKNOWN) { + events.error("UNAVAILABLE", "Charging status unavailable", null); + } else { + boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL; + events.success(isCharging ? "charging" : "discharging"); + } + } + }; + } + + private int getBatteryLevel() { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE); + return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY); + } else { + Intent intent = new ContextWrapper(getApplicationContext()). + registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + return (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) / + intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + } + } +} diff --git a/examples/platform_channel_swift/android/app/src/main/res/values/strings.xml b/examples/platform_channel_swift/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000000..e3aeb2f2fbb --- /dev/null +++ b/examples/platform_channel_swift/android/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Platform Channel + Flutter Application + diff --git a/examples/platform_channel_swift/android/build.gradle b/examples/platform_channel_swift/android/build.gradle new file mode 100644 index 00000000000..bb8a303898c --- /dev/null +++ b/examples/platform_channel_swift/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.2.1' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/examples/platform_channel_swift/android/gradle.properties b/examples/platform_channel_swift/android/gradle.properties new file mode 100644 index 00000000000..8bd86f68051 --- /dev/null +++ b/examples/platform_channel_swift/android/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx1536M diff --git a/examples/platform_channel_swift/android/gradle/wrapper/gradle-wrapper.properties b/examples/platform_channel_swift/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..2819f022f1f --- /dev/null +++ b/examples/platform_channel_swift/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/examples/platform_channel_swift/android/settings.gradle b/examples/platform_channel_swift/android/settings.gradle new file mode 100644 index 00000000000..115da6cb4f4 --- /dev/null +++ b/examples/platform_channel_swift/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withInputStream { stream -> plugins.load(stream) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/examples/platform_channel_swift/ios/Runner/AppDelegate.swift b/examples/platform_channel_swift/ios/Runner/AppDelegate.swift index 04ca3676644..937c8197465 100644 --- a/examples/platform_channel_swift/ios/Runner/AppDelegate.swift +++ b/examples/platform_channel_swift/ios/Runner/AppDelegate.swift @@ -31,7 +31,7 @@ enum MyFlutterErrorCode { fatalError("rootViewController is not type FlutterViewController") } let batteryChannel = FlutterMethodChannel(name: ChannelName.battery, - binaryMessenger: controller) + binaryMessenger: controller.binaryMessenger) batteryChannel.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in guard call.method == "getBatteryLevel" else { @@ -42,7 +42,7 @@ enum MyFlutterErrorCode { }) let chargingChannel = FlutterEventChannel(name: ChannelName.charging, - binaryMessenger: controller) + binaryMessenger: controller.binaryMessenger) chargingChannel.setStreamHandler(self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/examples/platform_channel_swift/pubspec.yaml b/examples/platform_channel_swift/pubspec.yaml index d735a37e245..41aebd92fb3 100644 --- a/examples/platform_channel_swift/pubspec.yaml +++ b/examples/platform_channel_swift/pubspec.yaml @@ -18,30 +18,58 @@ dev_dependencies: sdk: flutter flutter_driver: sdk: flutter + test: 1.6.3 + analyzer: 0.36.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + args: 1.5.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" charcode: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 5.0.8+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + html: 0.14.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + http: 0.12.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + http_multi_server: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + http_parser: 3.1.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" intl: 0.15.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + io: 0.3.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + js: 0.6.1+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" json_rpc_2: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + kernel: 0.3.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + mime: 0.9.6+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + multi_server_socket: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + node_preamble: 1.4.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + package_config: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + package_resolver: 1.0.10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.6.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pedantic: 1.8.0+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + pool: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pub_semver: 1.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" quiver: 2.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf: 0.7.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf_packages_handler: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf_static: 0.2.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf_web_socket: 0.2.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_map_stack_trace: 1.1.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_maps: 0.10.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.5.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" stack_trace: 1.9.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" stream_channel: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" test_api: 0.2.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.2.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + yaml: 2.1.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 3ffa +# PUBSPEC CHECKSUM: b1b5 diff --git a/examples/platform_channel_swift/test_driver/button_tap_test.dart b/examples/platform_channel_swift/test_driver/button_tap_test.dart index 974975990ad..b2e38b2ed13 100644 --- a/examples/platform_channel_swift/test_driver/button_tap_test.dart +++ b/examples/platform_channel_swift/test_driver/button_tap_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:flutter_driver/flutter_driver.dart'; -import 'package:test_api/test_api.dart' hide TypeMatcher, isInstanceOf; +import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; void main() { group('button tap test', () { @@ -19,18 +19,20 @@ void main() { }); test('tap on the button, verify result', () async { - final SerializableFinder batteryLevelLabel = find.byValueKey('Battery level label'); + final SerializableFinder batteryLevelLabel = + find.byValueKey('Battery level label'); expect(batteryLevelLabel, isNotNull); - final SerializableFinder button = find.text('Get Battery Level'); + final SerializableFinder button = find.text('Refresh'); await driver.waitFor(button); await driver.tap(button); String batteryLevel; - while (batteryLevel == null || batteryLevel.isEmpty) { + while (batteryLevel == null || batteryLevel.contains('unknown')) { batteryLevel = await driver.getText(batteryLevelLabel); } - expect(batteryLevel, isNotEmpty); + + expect(batteryLevel.contains('%'), isTrue); }); }); } diff --git a/examples/platform_view/ios/Podfile.lock b/examples/platform_view/ios/Podfile.lock index 1d0239cc659..b8ac2f42073 100644 --- a/examples/platform_view/ios/Podfile.lock +++ b/examples/platform_view/ios/Podfile.lock @@ -15,9 +15,9 @@ EXTERNAL SOURCES: :path: ".symlinks/flutter/ios" SPEC CHECKSUMS: - Flutter: 9d0fac939486c9aba2809b7982dfdbb47a7b0296 + Flutter: 58dd7d1b27887414a370fcccb9e645c08ffd7a6a MaterialControls: 1c6b29e78d3a13d8dd6a67ed31b6d26eb5de8f72 -PODFILE CHECKSUM: 4a320bf98e7f7e414d7d7f5079edf1b2d6679c9b +PODFILE CHECKSUM: 80af51c01ee3f0969ddbf2d1f0dcd6f44fad2c52 -COCOAPODS: 1.5.2 +COCOAPODS: 1.7.1 diff --git a/examples/platform_view/ios/Runner.xcodeproj/project.pbxproj b/examples/platform_view/ios/Runner.xcodeproj/project.pbxproj index 6682cd134f7..92a4706708b 100644 --- a/examples/platform_view/ios/Runner.xcodeproj/project.pbxproj +++ b/examples/platform_view/ios/Runner.xcodeproj/project.pbxproj @@ -40,11 +40,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0F803D4F4B1DB3E426346AD7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 2DAF064A1ED38C2300716BEE /* PlatformViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PlatformViewController.h; sourceTree = ""; }; 2DAF064B1ED38C3E00716BEE /* PlatformViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlatformViewController.m; sourceTree = ""; }; 2DAF064D1ED4224F00716BEE /* ic_add.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ic_add.png; sourceTree = ""; }; + 3036634A71F5F09C1B6453EC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 6E6555FD3971FC12A9802782 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -60,6 +62,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9D6BF03EDD3895D7B4DA4D7D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -79,6 +82,9 @@ 5A56E2F315C4CB64895375DA /* Pods */ = { isa = PBXGroup; children = ( + 3036634A71F5F09C1B6453EC /* Pods-Runner.debug.xcconfig */, + 9D6BF03EDD3895D7B4DA4D7D /* Pods-Runner.release.xcconfig */, + 0F803D4F4B1DB3E426346AD7 /* Pods-Runner.profile.xcconfig */, ); name = Pods; sourceTree = ""; @@ -236,7 +242,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { diff --git a/examples/stocks/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/stocks/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index ac48d350b41..f244429db54 100644 Binary files a/examples/stocks/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/examples/stocks/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/examples/stocks/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/stocks/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 50fcd554047..ac6f2d0d444 100644 Binary files a/examples/stocks/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/examples/stocks/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/examples/stocks/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/stocks/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 699852dd530..d0c9cd23d6e 100644 Binary files a/examples/stocks/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/examples/stocks/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/examples/stocks/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/stocks/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 436b03153b3..d907cf439c5 100644 Binary files a/examples/stocks/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/examples/stocks/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/examples/stocks/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/stocks/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index f2edf685a49..36430331af0 100644 Binary files a/examples/stocks/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/examples/stocks/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png index bc584542f54..bf55f17cd3a 100644 Binary files a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png and b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png index 409c1cefd5e..062b37a2c36 100644 Binary files a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png and b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png differ diff --git a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png index 8383cd80165..ddbe93215b1 100644 Binary files a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png and b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png differ diff --git a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png index 312b325dc14..929edb53276 100644 Binary files a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png and b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png differ diff --git a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png index af73fd12de3..1b8f3de80a7 100644 Binary files a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png and b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png differ diff --git a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@2x.png b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@2x.png index 93da7f02267..e9a6b95b166 100644 Binary files a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@2x.png and b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@2x.png differ diff --git a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png index 74b7d624e7e..28db17ea1a9 100644 Binary files a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png and b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png differ diff --git a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png index 93da7f02267..e9a6b95b166 100644 Binary files a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png and b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png differ diff --git a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png index dcedd29d267..865962ded4e 100644 Binary files a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png and b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png differ diff --git a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png index bc584542f54..bf55f17cd3a 100644 Binary files a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png and b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png differ diff --git a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png index 2c8dcf58803..a6cb12f8b1c 100644 Binary files a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png and b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png differ diff --git a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png index e4c3a937979..bc5f2949131 100644 Binary files a/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png and b/examples/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png differ diff --git a/examples/stocks/pubspec.yaml b/examples/stocks/pubspec.yaml index da48be70780..16dc3231e6b 100644 --- a/examples/stocks/pubspec.yaml +++ b/examples/stocks/pubspec.yaml @@ -21,8 +21,8 @@ dependencies: collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - dart_style: 1.2.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + dart_style: 1.2.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" html: 0.14.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -57,9 +57,9 @@ dev_dependencies: stream_channel: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" test_api: 0.2.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.0.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 4c79 +# PUBSPEC CHECKSUM: a57c diff --git a/examples/stocks/test/icon_color_test.dart b/examples/stocks/test/icon_color_test.dart index 5dedeb4a7f8..b36b214ae47 100644 --- a/examples/stocks/test/icon_color_test.dart +++ b/examples/stocks/test/icon_color_test.dart @@ -85,7 +85,7 @@ void main() { // check the color of the icon - dark mode checkIconColor(tester, 'Stock List', Colors.redAccent); // theme accent color - checkIconColor(tester, 'Account Balance', Colors.white30); // disabled + checkIconColor(tester, 'Account Balance', Colors.white38); // disabled checkIconColor(tester, 'About', Colors.white); // enabled }); } diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index 3c357f1d205..b43eb77a3be 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -68,6 +68,39 @@ enum OverlayVisibilityMode { always, } +class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder { + _CupertinoTextFieldSelectionGestureDetectorBuilder({ + @required _CupertinoTextFieldState state + }) : _state = state, + super(delegate: state); + + final _CupertinoTextFieldState _state; + + @override + void onSingleTapUp(TapUpDetails details) { + // Because TextSelectionGestureDetector listens to taps that happen on + // widgets in front of it, tapping the clear button will also trigger + // this handler. If the the clear button widget recognizes the up event, + // then do not handle it. + if (_state._clearGlobalKey.currentContext != null) { + final RenderBox renderBox = _state._clearGlobalKey.currentContext.findRenderObject(); + final Offset localOffset = renderBox.globalToLocal(details.globalPosition); + if (renderBox.hitTest(BoxHitTestResult(), position: localOffset)) { + return; + } + } + super.onSingleTapUp(details); + _state._requestKeyboard(); + if (_state.widget.onTap != null) + _state.widget.onTap(); + } + + @override + void onDragSelectionEnd(DragEndDetails details) { + _state._requestKeyboard(); + } +} + /// An iOS-style text field. /// /// A text field lets the user enter text, either with a hardware keyboard or with @@ -411,6 +444,12 @@ class CupertinoTextField extends StatefulWidget { final VoidCallback onEditingComplete; /// {@macro flutter.widgets.editableText.onSubmitted} + /// + /// See also: + /// + /// * [EditableText.onSubmitted] for an example of how to handle moving to + /// the next/previous field when using [TextInputAction.next] and + /// [TextInputAction.previous] for [textInputAction]. final ValueChanged onSubmitted; /// {@macro flutter.widgets.editableText.inputFormatters} @@ -500,9 +539,8 @@ class CupertinoTextField extends StatefulWidget { } } -class _CupertinoTextFieldState extends State with AutomaticKeepAliveClientMixin { +class _CupertinoTextFieldState extends State with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate { final GlobalKey _clearGlobalKey = GlobalKey(); - final GlobalKey _editableTextKey = GlobalKey(); TextEditingController _controller; TextEditingController get _effectiveController => widget.controller ?? _controller; @@ -510,17 +548,25 @@ class _CupertinoTextFieldState extends State with AutomaticK FocusNode _focusNode; FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); - // The selection overlay should only be shown when the user is interacting - // through a touch screen (via either a finger or a stylus). A mouse shouldn't - // trigger the selection overlay. - // For backwards-compatibility, we treat a null kind the same as touch. - bool _shouldShowSelectionToolbar = true; - bool _showSelectionHandles = false; + _CupertinoTextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; + + // API for TextSelectionGestureDetectorBuilderDelegate. + @override + bool get forcePressEnabled => true; + + @override + final GlobalKey editableTextKey = GlobalKey(); + + @override + bool get selectionEnabled => widget.selectionEnabled; + // End of API for TextSelectionGestureDetectorBuilderDelegate. + @override void initState() { super.initState(); + _selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder(state: this); if (widget.controller == null) { _controller = TextEditingController(); _controller.addListener(updateKeepAlive); @@ -550,103 +596,16 @@ class _CupertinoTextFieldState extends State with AutomaticK super.dispose(); } - EditableTextState get _editableText => _editableTextKey.currentState; + EditableTextState get _editableText => editableTextKey.currentState; void _requestKeyboard() { _editableText?.requestKeyboard(); } - RenderEditable get _renderEditable => _editableText.renderEditable; - - void _handleTapDown(TapDownDetails details) { - _renderEditable.handleTapDown(details); - - // The selection overlay should only be shown when the user is interacting - // through a touch screen (via either a finger or a stylus). A mouse shouldn't - // trigger the selection overlay. - // For backwards-compatibility, we treat a null kind the same as touch. - final PointerDeviceKind kind = details.kind; - _shouldShowSelectionToolbar = - kind == null || - kind == PointerDeviceKind.touch || - kind == PointerDeviceKind.stylus; - } - - void _handleForcePressStarted(ForcePressDetails details) { - if (widget.selectionEnabled) { - _renderEditable.selectWordsInRange( - from: details.globalPosition, - cause: SelectionChangedCause.forcePress, - ); - } - } - - void _handleForcePressEnded(ForcePressDetails details) { - _renderEditable.selectWordsInRange( - from: details.globalPosition, - cause: SelectionChangedCause.forcePress, - ); - if (_shouldShowSelectionToolbar) - _editableText.showToolbar(); - } - - void _handleSingleTapUp(TapUpDetails details) { - // Because TextSelectionGestureDetector listens to taps that happen on - // widgets in front of it, tapping the clear button will also trigger - // this handler. If the the clear button widget recognizes the up event, - // then do not handle it. - if (_clearGlobalKey.currentContext != null) { - final RenderBox renderBox = _clearGlobalKey.currentContext.findRenderObject(); - final Offset localOffset = renderBox.globalToLocal(details.globalPosition); - if(renderBox.hitTest(BoxHitTestResult(), position: localOffset)) { - return; - } - } - - if (widget.selectionEnabled) { - _renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); - } - _requestKeyboard(); - if (widget.onTap != null) { - widget.onTap(); - } - } - - void _handleSingleLongTapStart(LongPressStartDetails details) { - if (widget.selectionEnabled) { - _renderEditable.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.longPress, - ); - } - } - - void _handleSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { - if (widget.selectionEnabled) { - _renderEditable.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.longPress, - ); - } - } - - void _handleSingleLongTapEnd(LongPressEndDetails details) { - if (_shouldShowSelectionToolbar) - _editableText.showToolbar(); - } - - void _handleDoubleTapDown(TapDownDetails details) { - if (widget.selectionEnabled) { - _renderEditable.selectWord(cause: SelectionChangedCause.tap); - if (_shouldShowSelectionToolbar) - _editableText.showToolbar(); - } - } - bool _shouldShowSelectionHandles(SelectionChangedCause cause) { // When the text field is activated by something that doesn't trigger the // selection overlay, we shouldn't show the handles either. - if (!_shouldShowSelectionToolbar) + if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) return false; // On iOS, we don't show handles when the selection is collapsed. @@ -662,28 +621,6 @@ class _CupertinoTextFieldState extends State with AutomaticK return false; } - void _handleMouseDragSelectionStart(DragStartDetails details) { - _renderEditable.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.drag, - ); - } - - void _handleMouseDragSelectionUpdate( - DragStartDetails startDetails, - DragUpdateDetails updateDetails, - ) { - _renderEditable.selectPositionAt( - from: startDetails.globalPosition, - to: updateDetails.globalPosition, - cause: SelectionChangedCause.drag, - ); - } - - void _handleMouseDragSelectionEnd(DragEndDetails details) { - _requestKeyboard(); - } - void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) { if (cause == SelectionChangedCause.longPress) { _editableText?.bringIntoView(selection.base); @@ -864,7 +801,7 @@ class _CupertinoTextFieldState extends State with AutomaticK padding: widget.padding, child: RepaintBoundary( child: EditableText( - key: _editableTextKey, + key: editableTextKey, controller: controller, readOnly: widget.readOnly, showCursor: widget.showCursor, @@ -919,18 +856,7 @@ class _CupertinoTextFieldState extends State with AutomaticK ignoring: !enabled, child: Container( decoration: effectiveDecoration, - child: TextSelectionGestureDetector( - onTapDown: _handleTapDown, - onForcePressStart: _handleForcePressStarted, - onForcePressEnd: _handleForcePressEnded, - onSingleTapUp: _handleSingleTapUp, - onSingleLongTapStart: _handleSingleLongTapStart, - onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate, - onSingleLongTapEnd: _handleSingleLongTapEnd, - onDoubleTapDown: _handleDoubleTapDown, - onDragSelectionStart: _handleMouseDragSelectionStart, - onDragSelectionUpdate: _handleMouseDragSelectionUpdate, - onDragSelectionEnd: _handleMouseDragSelectionEnd, + child: _selectionGestureDetectorBuilder.buildGestureDetector( behavior: HitTestBehavior.translucent, child: Align( alignment: Alignment(-1.0, _textAlignVertical.y), diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index e1b0771e372..22c1c6a0bd7 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -34,6 +34,19 @@ const TextStyle _errorTextStyle = TextStyle( debugLabel: 'fallback style; consider putting your text in a Material', ); +/// Describes which theme will be used by [MaterialApp]. +enum ThemeMode { + /// Use either the light or dark theme based on what the user has selected in + /// the system settings. + system, + + /// Always use the light mode regardless of system preference. + light, + + /// Always use the dark mode (if available) regardless of system preference. + dark, +} + /// An application that uses material design. /// /// A convenience widget that wraps a number of widgets that are commonly @@ -99,6 +112,7 @@ class MaterialApp extends StatefulWidget { this.color, this.theme, this.darkTheme, + this.themeMode = ThemeMode.system, this.locale, this.localizationsDelegates, this.localeListResolutionCallback, @@ -169,13 +183,15 @@ class MaterialApp extends StatefulWidget { /// Default visual properties, like colors fonts and shapes, for this app's /// material widgets. /// - /// A second [darkTheme] [ThemeData] value, which is used when the underlying - /// platform requests a "dark mode" UI, can also be specified. + /// A second [darkTheme] [ThemeData] value, which is used to provide a dark + /// version of the user interface can also be specified. [themeMode] will + /// control which theme will be used if a [darkTheme] is provided. /// /// The default value of this property is the value of [ThemeData.light()]. /// /// See also: /// + /// * [themeMode], which controls which theme to use. /// * [MediaQueryData.platformBrightness], which indicates the platform's /// desired brightness and is used to automatically toggle between [theme] /// and [darkTheme] in [MaterialApp]. @@ -183,20 +199,21 @@ class MaterialApp extends StatefulWidget { /// colors. final ThemeData theme; - /// The [ThemeData] to use when the platform specifically requests a dark - /// themed UI. + /// The [ThemeData] to use when a 'dark mode' is requested by the system. /// - /// Host platforms such as Android Pie can request a system-wide "dark mode" - /// when entering battery saver mode. + /// Some host platforms allow the users to select a system-wide 'dark mode', + /// or the application may want to offer the user the ability to choose a + /// dark theme just for this application. This is theme that will be used for + /// such cases. [themeMode] will control which theme will be used. /// - /// When the host platform requests a [Brightness.dark] mode, you may want to - /// supply a [ThemeData.brightness] that's also [Brightness.dark]. + /// This theme should have a [ThemeData.brightness] set to [Brightness.dark]. /// /// Uses [theme] instead when null. Defaults to the value of /// [ThemeData.light()] when both [darkTheme] and [theme] are null. /// /// See also: /// + /// * [themeMode], which controls which theme to use. /// * [MediaQueryData.platformBrightness], which indicates the platform's /// desired brightness and is used to automatically toggle between [theme] /// and [darkTheme] in [MaterialApp]. @@ -204,6 +221,32 @@ class MaterialApp extends StatefulWidget { /// [MediaQueryData.platformBrightness]. final ThemeData darkTheme; + /// Determines which theme will be used by the application if both [theme] + /// and [darkTheme] are provided. + /// + /// If set to [ThemeMode.system], the choice of which theme to use will + /// be based on the user's system preferences. If the [MediaQuery.platformBrightnessOf] + /// is [Brightness.light], [theme] will be used. If it is [Brightness.dark], + /// [darkTheme] will be used (unless it is [null], in which case [theme] + /// will be used. + /// + /// If set to [ThemeMode.light] the [theme] will always be used, + /// regardless of the user's system preference. + /// + /// If set to [ThemeMode.dark] the [darkTheme] will be used + /// regardless of the user's system preference. If [darkTheme] is [null] + /// then it will fallback to using [theme]. + /// + /// The default value is [ThemeMode.system]. + /// + /// See also: + /// + /// * [theme], which is used when a light mode is selected. + /// * [darkTheme], which is used when a dark mode is selected. + /// * [ThemeData.brightness], which indicates to various parts of the + /// system what kind of theme is being used. + final ThemeMode themeMode; + /// {@macro flutter.widgets.widgetsApp.color} final Color color; @@ -454,15 +497,16 @@ class _MaterialAppState extends State { onUnknownRoute: widget.onUnknownRoute, builder: (BuildContext context, Widget child) { // Use a light theme, dark theme, or fallback theme. + final ThemeMode mode = widget.themeMode ?? ThemeMode.system; ThemeData theme; - final ui.Brightness platformBrightness = MediaQuery.platformBrightnessOf(context); - if (platformBrightness == ui.Brightness.dark && widget.darkTheme != null) { - theme = widget.darkTheme; - } else if (widget.theme != null) { - theme = widget.theme; - } else { - theme = ThemeData.fallback(); + if (widget.darkTheme != null) { + final ui.Brightness platformBrightness = MediaQuery.platformBrightnessOf(context); + if (mode == ThemeMode.dark || + (mode == ThemeMode.system && platformBrightness == ui.Brightness.dark)) { + theme = widget.darkTheme; + } } + theme ??= widget.theme ?? ThemeData.fallback(); return AnimatedTheme( data: theme, diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index 8f9ca2aae71..7ede1d9864b 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -400,6 +400,11 @@ class _ModalBottomSheetRoute extends PopupRoute { /// a [GridView] and have the bottom sheet be draggable, you should set this /// parameter to true. /// +/// The `useRootNavigator` parameter ensures that the root navigator is used to +/// display the [BottomSheet] when set to `true`. This is useful in the case +/// that a modal [BottomSheet] needs to be displayed above all other content +/// but the caller is inside another [Navigator]. +/// /// Returns a `Future` that resolves to the value (if any) that was passed to /// [Navigator.pop] when the modal bottom sheet was closed. /// @@ -417,14 +422,16 @@ Future showModalBottomSheet({ double elevation, ShapeBorder shape, bool isScrollControlled = false, + bool useRootNavigator = false, }) { assert(context != null); assert(builder != null); assert(isScrollControlled != null); + assert(useRootNavigator != null); assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMaterialLocalizations(context)); - return Navigator.push(context, _ModalBottomSheetRoute( + return Navigator.of(context, rootNavigator: useRootNavigator).push(_ModalBottomSheetRoute( builder: builder, theme: Theme.of(context, shadowThemeOnly: true), isScrollControlled: isScrollControlled, diff --git a/packages/flutter/lib/src/material/button_theme.dart b/packages/flutter/lib/src/material/button_theme.dart index 742f9e2b9c1..0102034a1c4 100644 --- a/packages/flutter/lib/src/material/button_theme.dart +++ b/packages/flutter/lib/src/material/button_theme.dart @@ -468,18 +468,12 @@ class ButtonThemeData extends Diagnosticable { return button.textTheme ?? textTheme; } - Color _getDisabledColor(MaterialButton button) { - return getBrightness(button) == Brightness.dark - ? colorScheme.onSurface.withOpacity(0.30) // default == Colors.white30 - : colorScheme.onSurface.withOpacity(0.38); // default == Colors.black38; - } - /// The foreground color of the [button]'s text and icon when /// [MaterialButton.onPressed] is null (when MaterialButton.enabled is false). /// /// Returns the button's [MaterialButton.disabledColor] if it is non-null. /// Otherwise the color scheme's [ColorScheme.onSurface] color is returned - /// with its opacity set to 0.30 if [getBrightness] is dark, 0.38 otherwise. + /// with its opacity set to 0.38. /// /// If [MaterialButton.textColor] is a [MaterialStateProperty], it will be /// used as the `disabledTextColor`. It will be resolved in the [MaterialState.disabled] state. @@ -488,7 +482,7 @@ class ButtonThemeData extends Diagnosticable { return button.textColor; if (button.disabledTextColor != null) return button.disabledTextColor; - return _getDisabledColor(button); + return colorScheme.onSurface.withOpacity(0.38); } /// The [button]'s background color when [MaterialButton.onPressed] is null @@ -500,13 +494,13 @@ class ButtonThemeData extends Diagnosticable { /// is returned, if it is non-null. /// /// Otherwise the color scheme's [ColorScheme.onSurface] color is returned - /// with its opacity set to 0.3 if [getBrightness] is dark, 0.38 otherwise. + /// with its opacity set to 0.38. Color getDisabledFillColor(MaterialButton button) { if (button.disabledColor != null) return button.disabledColor; if (_disabledColor != null) return _disabledColor; - return _getDisabledColor(button); + return colorScheme.onSurface.withOpacity(0.38); } /// The button's background fill color or null for buttons that don't have diff --git a/packages/flutter/lib/src/material/colors.dart b/packages/flutter/lib/src/material/colors.dart index 7f2131d6c0b..6c2fbd75cd4 100644 --- a/packages/flutter/lib/src/material/colors.dart +++ b/packages/flutter/lib/src/material/colors.dart @@ -303,7 +303,7 @@ class Colors { /// * [Typography.white], which uses this color for its text styles. /// * [Theme.of], which allows you to select colors from the current theme /// rather than hard-coding colors in your build methods. - /// * [white70, white60, white54, white30, white12, white10], which are variants on this color + /// * [white70, white60, white54, white38, white30, white12, white10], which are variants on this color /// but with different opacities. /// * [black], a solid black color. /// * [transparent], a fully-transparent color. @@ -320,7 +320,7 @@ class Colors { /// * [Typography.white], which uses this color for its text styles. /// * [Theme.of], which allows you to select colors from the current theme /// rather than hard-coding colors in your build methods. - /// * [white, white60, white54, white30, white12, white10], which are variants on this color + /// * [white, white60, white54, white38, white30, white12, white10], which are variants on this color /// but with different opacities. static const Color white70 = Color(0xB3FFFFFF); @@ -336,7 +336,7 @@ class Colors { /// * [ExpandIcon], which uses this color for dark themes. /// * [Theme.of], which allows you to select colors from the current theme /// rather than hard-coding colors in your build methods. - /// * [white, white54, white30, white12, white10], which are variants on this color + /// * [white, white54, white30, white38, white12, white10], which are variants on this color /// but with different opacities. static const Color white60 = Color(0x99FFFFFF); @@ -348,11 +348,11 @@ class Colors { /// /// * [Theme.of], which allows you to select colors from the current theme /// rather than hard-coding colors in your build methods. - /// * [white, white60, white30, white12, white10], which are variants on this color + /// * [white, white60, white38, white30, white12, white10], which are variants on this color /// but with different opacities. static const Color white54 = Color(0x8AFFFFFF); - /// White with 30% opacity. + /// White with 38% opacity. /// /// Used for disabled radio buttons and the text of disabled flat buttons in dark themes. /// @@ -363,7 +363,19 @@ class Colors { /// * [ThemeData.disabledColor], which uses this color by default in dark themes. /// * [Theme.of], which allows you to select colors from the current theme /// rather than hard-coding colors in your build methods. - /// * [white, white60, white54, white70, white12, white10], which are variants on this color + /// * [white, white60, white54, white70, white30, white12, white10], which are variants on this color + /// but with different opacities. + static const Color white38 = Color(0x62FFFFFF); + + /// White with 30% opacity. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/Colors.whites.png) + /// + /// See also: + /// + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. + /// * [white, white60, white54, white70, white38, white12, white10], which are variants on this color /// but with different opacities. static const Color white30 = Color(0x4DFFFFFF); @@ -375,7 +387,7 @@ class Colors { /// /// See also: /// - /// * [white, white60, white54, white70, white30, white10], which are variants on this color + /// * [white, white60, white54, white70, white38, white30, white10], which are variants on this color /// but with different opacities. static const Color white24 = Color(0x3DFFFFFF); @@ -387,7 +399,7 @@ class Colors { /// /// See also: /// - /// * [white, white60, white54, white70, white30, white10], which are variants on this color + /// * [white, white60, white54, white70, white38, white30, white10], which are variants on this color /// but with different opacities. static const Color white12 = Color(0x1FFFFFFF); @@ -397,7 +409,7 @@ class Colors { /// /// See also: /// - /// * [white, white60, white54, white70, white30, white12], which are variants on this color + /// * [white, white60, white54, white70, white38, white30, white12], which are variants on this color /// but with different opacities. /// * [transparent], a fully-transparent color, not far from this one. static const Color white10 = Color(0x1AFFFFFF); diff --git a/packages/flutter/lib/src/material/data_table.dart b/packages/flutter/lib/src/material/data_table.dart index 47ad31fce06..60822095313 100644 --- a/packages/flutter/lib/src/material/data_table.dart +++ b/packages/flutter/lib/src/material/data_table.dart @@ -505,7 +505,7 @@ class DataTable extends StatelessWidget { fontSize: 13.0, color: isLightTheme ? (placeholder ? Colors.black38 : Colors.black87) - : (placeholder ? Colors.white30 : Colors.white70), + : (placeholder ? Colors.white38 : Colors.white70), ), child: IconTheme.merge( data: IconThemeData( diff --git a/packages/flutter/lib/src/material/expand_icon.dart b/packages/flutter/lib/src/material/expand_icon.dart index 6f6e52f464c..488f3df38d8 100644 --- a/packages/flutter/lib/src/material/expand_icon.dart +++ b/packages/flutter/lib/src/material/expand_icon.dart @@ -81,7 +81,7 @@ class ExpandIcon extends StatefulWidget { /// /// Defaults to [Colors.black38] when the theme's /// [ThemeData.brightness] is [Brightness.light] and to - /// [Colors.white30] when it is [Brightness.dark]. This adheres to the + /// [Colors.white38] when it is [Brightness.dark]. This adheres to the /// Material Design specifications for [icons](https://material.io/design/iconography/system-icons.html#color) /// and for [dark theme](https://material.io/design/color/dark-theme.html#ui-application) final Color disabledColor; diff --git a/packages/flutter/lib/src/material/stepper.dart b/packages/flutter/lib/src/material/stepper.dart index 291c445d924..a63a7dada9c 100644 --- a/packages/flutter/lib/src/material/stepper.dart +++ b/packages/flutter/lib/src/material/stepper.dart @@ -62,7 +62,7 @@ final Color _kErrorDark = Colors.red.shade400; const Color _kCircleActiveLight = Colors.white; const Color _kCircleActiveDark = Colors.black87; const Color _kDisabledLight = Colors.black38; -const Color _kDisabledDark = Colors.white30; +const Color _kDisabledDark = Colors.white38; const double _kStepSize = 24.0; const double _kTriangleHeight = _kStepSize * 0.866025; // Triangle height. sqrt(3.0) / 2.0 diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index cce50a12caf..ffb715b8d37 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -35,6 +35,107 @@ typedef InputCounterWidgetBuilder = Widget Function( @required bool isFocused, }); +class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder { + _TextFieldSelectionGestureDetectorBuilder({ + @required _TextFieldState state + }) : _state = state, + super(delegate: state); + + final _TextFieldState _state; + + @override + void onTapDown(TapDownDetails details) { + super.onTapDown(details); + _state._startSplash(details.globalPosition); + } + + @override + void onForcePressStart(ForcePressDetails details) { + super.onForcePressStart(details); + if (delegate.selectionEnabled && shouldShowSelectionToolbar) { + editableText.showToolbar(); + } + } + + @override + void onForcePressEnd(ForcePressDetails details) { + // Not required. + } + + @override + void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { + if (delegate.selectionEnabled) { + switch (Theme.of(_state.context).platform) { + case TargetPlatform.iOS: + renderEditable.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.longPress, + ); + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + renderEditable.selectWordsInRange( + from: details.globalPosition - details.offsetFromOrigin, + to: details.globalPosition, + cause: SelectionChangedCause.longPress, + ); + break; + } + } + } + + @override + void onSingleTapUp(TapUpDetails details) { + editableText.hideToolbar(); + if (delegate.selectionEnabled) { + switch (Theme.of(_state.context).platform) { + case TargetPlatform.iOS: + renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + renderEditable.selectPosition(cause: SelectionChangedCause.tap); + break; + } + } + _state._requestKeyboard(); + _state._confirmCurrentSplash(); + if (_state.widget.onTap != null) + _state.widget.onTap(); + } + + @override + void onSingleTapCancel() { + _state._cancelCurrentSplash(); + } + + @override + void onSingleLongTapStart(LongPressStartDetails details) { + if (delegate.selectionEnabled) { + switch (Theme.of(_state.context).platform) { + case TargetPlatform.iOS: + renderEditable.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.longPress, + ); + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + renderEditable.selectWord(cause: SelectionChangedCause.longPress); + Feedback.forLongPress(_state.context); + break; + } + } + _state._confirmCurrentSplash(); + } + + @override + void onDragSelectionStart(DragStartDetails details) { + super.onDragSelectionStart(details); + _state._startSplash(details.globalPosition); + } +} + /// A material design text field. /// /// A text field lets the user enter text, either with hardware keyboard or with @@ -388,6 +489,12 @@ class TextField extends StatefulWidget { final VoidCallback onEditingComplete; /// {@macro flutter.widgets.editableText.onSubmitted} + /// + /// See also: + /// + /// * [EditableText.onSubmitted] for an example of how to handle moving to + /// the next/previous field when using [TextInputAction.next] and + /// [TextInputAction.previous] for [textInputAction]. final ValueChanged onSubmitted; /// {@macro flutter.widgets.editableText.inputFormatters} @@ -525,9 +632,7 @@ class TextField extends StatefulWidget { } } -class _TextFieldState extends State with AutomaticKeepAliveClientMixin { - final GlobalKey _editableTextKey = GlobalKey(); - +class _TextFieldState extends State with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate { Set _splashes; InteractiveInkFeature _currentSplash; @@ -543,10 +648,21 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi && widget.decoration != null && widget.decoration.counterText == null; - bool _shouldShowSelectionToolbar = true; - bool _showSelectionHandles = false; + _TextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; + + // API for TextSelectionGestureDetectorBuilderDelegate. + @override + bool forcePressEnabled; + + @override + final GlobalKey editableTextKey = GlobalKey(); + + @override + bool get selectionEnabled => widget.selectionEnabled; + // End of API for TextSelectionGestureDetectorBuilderDelegate. + InputDecoration _getEffectiveDecoration() { final MaterialLocalizations localizations = MaterialLocalizations.of(context); final ThemeData themeData = Theme.of(context); @@ -615,6 +731,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi @override void initState() { super.initState(); + _selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this); if (widget.controller == null) _controller = TextEditingController(); } @@ -644,7 +761,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi super.dispose(); } - EditableTextState get _editableText => _editableTextKey.currentState; + EditableTextState get _editableText => editableTextKey.currentState; void _requestKeyboard() { _editableText?.requestKeyboard(); @@ -653,7 +770,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi bool _shouldShowSelectionHandles(SelectionChangedCause cause) { // When the text field is activated by something that doesn't trigger the // selection overlay, we shouldn't show the handles either. - if (!_shouldShowSelectionToolbar) + if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) return false; if (cause == SelectionChangedCause.keyboard) @@ -701,7 +818,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi InteractiveInkFeature _createInkFeature(Offset globalPosition) { final MaterialInkController inkController = Material.of(context); final ThemeData themeData = Theme.of(context); - final BuildContext editableContext = _editableTextKey.currentContext; + final BuildContext editableContext = editableTextKey.currentContext; final RenderBox referenceBox = InputDecorator.containerOf(editableContext) ?? editableContext.findRenderObject(); final Offset position = referenceBox.globalToLocal(globalPosition); final Color color = themeData.splashColor; @@ -732,132 +849,6 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi return splash; } - RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable; - - void _handleTapDown(TapDownDetails details) { - _renderEditable.handleTapDown(details); - _startSplash(details.globalPosition); - - // The selection overlay should only be shown when the user is interacting - // through a touch screen (via either a finger or a stylus). A mouse shouldn't - // trigger the selection overlay. - // For backwards-compatibility, we treat a null kind the same as touch. - final PointerDeviceKind kind = details.kind; - _shouldShowSelectionToolbar = - kind == null || - kind == PointerDeviceKind.touch || - kind == PointerDeviceKind.stylus; - } - - void _handleForcePressStarted(ForcePressDetails details) { - if (widget.selectionEnabled) { - _renderEditable.selectWordsInRange( - from: details.globalPosition, - cause: SelectionChangedCause.forcePress, - ); - if (_shouldShowSelectionToolbar) { - _editableTextKey.currentState.showToolbar(); - } - } - } - - void _handleSingleTapUp(TapUpDetails details) { - if (widget.selectionEnabled) { - switch (Theme.of(context).platform) { - case TargetPlatform.iOS: - _renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); - break; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - _renderEditable.selectPosition(cause: SelectionChangedCause.tap); - break; - } - } - _requestKeyboard(); - _confirmCurrentSplash(); - if (widget.onTap != null) - widget.onTap(); - } - - void _handleSingleTapCancel() { - _cancelCurrentSplash(); - } - - void _handleSingleLongTapStart(LongPressStartDetails details) { - if (widget.selectionEnabled) { - switch (Theme.of(context).platform) { - case TargetPlatform.iOS: - _renderEditable.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.longPress, - ); - break; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - _renderEditable.selectWord(cause: SelectionChangedCause.longPress); - Feedback.forLongPress(context); - break; - } - } - _confirmCurrentSplash(); - } - - void _handleSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { - if (widget.selectionEnabled) { - switch (Theme.of(context).platform) { - case TargetPlatform.iOS: - _renderEditable.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.longPress, - ); - break; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - _renderEditable.selectWordsInRange( - from: details.globalPosition - details.offsetFromOrigin, - to: details.globalPosition, - cause: SelectionChangedCause.longPress, - ); - break; - } - } - } - - void _handleSingleLongTapEnd(LongPressEndDetails details) { - if (widget.selectionEnabled) { - if (_shouldShowSelectionToolbar) - _editableTextKey.currentState.showToolbar(); - } - } - - void _handleDoubleTapDown(TapDownDetails details) { - if (widget.selectionEnabled) { - _renderEditable.selectWord(cause: SelectionChangedCause.doubleTap); - if (_shouldShowSelectionToolbar) { - _editableText.showToolbar(); - } - } - } - - void _handleMouseDragSelectionStart(DragStartDetails details) { - _renderEditable.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.drag, - ); - _startSplash(details.globalPosition); - } - - void _handleMouseDragSelectionUpdate( - DragStartDetails startDetails, - DragUpdateDetails updateDetails, - ) { - _renderEditable.selectPositionAt( - from: startDetails.globalPosition, - to: updateDetails.globalPosition, - cause: SelectionChangedCause.drag, - ); - } - void _startSplash(Offset globalPosition) { if (_effectiveFocusNode.hasFocus) return; @@ -926,7 +917,6 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi if (widget.maxLength != null && widget.maxLengthEnforced) formatters.add(LengthLimitingTextInputFormatter(widget.maxLength)); - bool forcePressEnabled; TextSelectionControls textSelectionControls; bool paintCursorAboveText; bool cursorOpacityAnimates; @@ -964,7 +954,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi Widget child = RepaintBoundary( child: EditableText( - key: _editableTextKey, + key: editableTextKey, readOnly: widget.readOnly, showCursor: widget.showCursor, showSelectionHandles: _showSelectionHandles, @@ -1039,17 +1029,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi onPointerExit: _handlePointerExit, child: IgnorePointer( ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true), - child: TextSelectionGestureDetector( - onTapDown: _handleTapDown, - onForcePressStart: forcePressEnabled ? _handleForcePressStarted : null, - onSingleTapUp: _handleSingleTapUp, - onSingleTapCancel: _handleSingleTapCancel, - onSingleLongTapStart: _handleSingleLongTapStart, - onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate, - onSingleLongTapEnd: _handleSingleLongTapEnd, - onDoubleTapDown: _handleDoubleTapDown, - onDragSelectionStart: _handleMouseDragSelectionStart, - onDragSelectionUpdate: _handleMouseDragSelectionUpdate, + child: _selectionGestureDetectorBuilder.buildGestureDetector( behavior: HitTestBehavior.translucent, child: child, ), diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 62cb7a53ac3..53dc55883c7 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -249,7 +249,7 @@ class ThemeData extends Diagnosticable { splashColor: splashColor, materialTapTargetSize: materialTapTargetSize, ); - disabledColor ??= isDark ? Colors.white30 : Colors.black38; + disabledColor ??= isDark ? Colors.white38 : Colors.black38; highlightColor ??= isDark ? _kDarkThemeHighlightColor : _kLightThemeHighlightColor; splashColor ??= isDark ? _kDarkThemeSplashColor : _kLightThemeSplashColor; diff --git a/packages/flutter/lib/src/painting/text_style.dart b/packages/flutter/lib/src/painting/text_style.dart index 7f8a9969302..fb9fa670f26 100644 --- a/packages/flutter/lib/src/painting/text_style.dart +++ b/packages/flutter/lib/src/painting/text_style.dart @@ -130,14 +130,10 @@ const String _kColorBackgroundWarning = 'Cannot provide both a backgroundColor a /// ``` /// {@end-tool} /// -/// {@tool sample} -/// /// Examples of the resulting heights from different values of `TextStyle.height`: /// /// ![Text height comparison diagram](https://flutter.github.io/assets-for-api-docs/assets/painting/text_height_comparison_diagram.png) /// -/// {@end-tool} -/// /// ### Wavy red underline with black text /// /// {@tool sample} diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index e41b6c0a509..22ef5d0d700 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart @@ -1196,6 +1196,45 @@ class ClipPathLayer extends ContainerLayer { } } +/// A composite layer that applies a [ColorFilter] to its children. +class ColorFilterLayer extends ContainerLayer { + /// Creates a layer that applies a [ColorFilter] to its children. + /// + /// The [ColorFilter] property must be non-null before the compositing phase + /// of the pipeline. + ColorFilterLayer({ + @required ColorFilter colorFilter, + }) : _colorFilter = colorFilter, + assert(colorFilter != null); + + /// The color filter to apply to children. + /// + /// The scene must be explicitly recomposited after this property is changed + /// (as described at [Layer]). + ColorFilter get colorFilter => _colorFilter; + ColorFilter _colorFilter; + set colorFilter(ColorFilter value) { + if (value != _colorFilter) { + _colorFilter = value; + markNeedsAddToScene(); + } + } + + @override + ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { + builder.pushColorFilter(colorFilter); + addChildrenToScene(builder, layerOffset); + builder.pop(); + return null; // this does not return an engine layer yet. + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('colorFilter', colorFilter)); + } +} + /// A composited layer that applies a given transformation matrix to its /// children. /// diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 3aeb6d83dde..8d9424f3512 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -444,6 +444,24 @@ class PaintingContext extends ClipContext { } } + /// Blend further painting with a color filter. + /// + /// * `offset` is the offset from the origin of the canvas' coordinate system + /// to the origin of the caller's coordinate system. + /// * `colorFilter` is the [ColorFilter] value to use when blending the + /// painting done by `painter`. + /// * `painter` is a callback that will paint with the `colorFilter` applied. + /// This function calls the `painter` synchronously. + /// + /// A [RenderObject] that uses this function is very likely to require its + /// [RenderObject.alwaysNeedsCompositing] property to return true. That informs + /// ancestor render objects that this render object will include a composited + /// layer, which, for example, causes them to use composited clips. + void pushColorFilter(Offset offset, ColorFilter colorFilter, PaintingContextCallback painter) { + assert(colorFilter != null); + pushLayer(ColorFilterLayer(colorFilter: colorFilter), painter, offset); + } + /// Transform further painting using a matrix. /// /// * `needsCompositing` is whether the child needs compositing. Typically diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 12f910f0def..3e6f2e52ed6 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -4713,13 +4713,14 @@ class Wrap extends MultiChildRenderObjectWidget { /// children. /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). /// +/// +/// {@animation 450 100 https://flutter.github.io/assets-for-api-docs/assets/widgets/flow_menu.mp4} +/// /// {@tool snippet --template=freeform} /// /// This example uses the [Flow] widget to create a menu that opens and closes -/// as it is interacted with. The color of the button in the menu changes to -/// indicate which one has been selected. -/// -/// {@animation 450 100 https://flutter.github.io/assets-for-api-docs/assets/widgets/flow_menu.mp4} +/// as it is interacted with, shown above. The color of the button in the menu +/// changes to indicate which one has been selected. /// /// ```dart main /// import 'package:flutter/material.dart'; diff --git a/packages/flutter/lib/src/widgets/color_filter.dart b/packages/flutter/lib/src/widgets/color_filter.dart new file mode 100644 index 00000000000..923784d23a2 --- /dev/null +++ b/packages/flutter/lib/src/widgets/color_filter.dart @@ -0,0 +1,60 @@ +// Copyright 2019 The Flutter 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:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'framework.dart'; + +/// Applies a [ColorFilter] to its child. +@immutable +class ColorFiltered extends SingleChildRenderObjectWidget { + /// Creates a widget that applies a [ColorFilter] to its child. + /// + /// The [colorFilter] must not be null. + const ColorFiltered({@required this.colorFilter, Widget child, Key key}) + : assert(colorFilter != null), + super(key: key, child: child); + + /// The color filter to apply to the child of this widvget. + final ColorFilter colorFilter; + + @override + RenderObject createRenderObject(BuildContext context) => _ColorFilterRenderObject(colorFilter); + + @override + void updateRenderObject(BuildContext context, _ColorFilterRenderObject renderObject) { + renderObject..colorFilter = colorFilter; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('colorFilter', colorFilter)); + } +} + +class _ColorFilterRenderObject extends RenderProxyBox { + _ColorFilterRenderObject(this._colorFilter); + + ColorFilter get colorFilter => _colorFilter; + ColorFilter _colorFilter; + set colorFilter(ColorFilter value) { + assert(value != null); + if (value != _colorFilter) { + _colorFilter = value; + markNeedsPaint(); + } + } + + @override + bool get alwaysNeedsCompositing => child != null; + + @override + void paint(PaintingContext context, Offset offset) { + context.pushColorFilter(offset, colorFilter, super.paint); + } +} diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 36809a859de..468b0a834b5 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -101,7 +101,7 @@ const int _kObscureShowLatestCharCursorTicks = 3; /// padding: const EdgeInsets.all(6), /// child: TextFormField( /// controller: _controller, -/// decoration: InputDecoration(border: OutlineInputBorder()), +/// decoration: InputDecoration(border: OutlineInputBorder()), /// ), /// ), /// ); @@ -688,6 +688,63 @@ class EditableText extends StatefulWidget { /// Called when the user indicates that they are done editing the text in the /// field. /// {@endtemplate} + /// + /// {@tool snippet --template=stateful_widget_material} + /// When a non-completion action is pressed, such as "next" or "previous", it + /// is often desirable to move the focus to the next or previous field. To do + /// this, handle it as in this example, by calling [FocusNode.focusNext] in + /// the [TextFormField.onFieldSubmitted] callback ([TextFormField] wraps + /// [EditableText] internally, and uses the value of `onFieldSubmitted` as its + /// [onSubmitted]). + /// + /// ```dart + /// FocusScopeNode _focusScopeNode = FocusScopeNode(); + /// final _controller1 = TextEditingController(); + /// final _controller2 = TextEditingController(); + /// + /// void dispose() { + /// _focusScopeNode.dispose(); + /// _controller1.dispose(); + /// _controller2.dispose(); + /// super.dispose(); + /// } + /// + /// void _handleSubmitted(String value) { + /// _focusScopeNode.nextFocus(); + /// } + /// + /// Widget build(BuildContext context) { + /// return Scaffold( + /// body: FocusScope( + /// node: _focusScopeNode, + /// child: Column( + /// mainAxisAlignment: MainAxisAlignment.center, + /// children: [ + /// Padding( + /// padding: const EdgeInsets.all(8.0), + /// child: TextFormField( + /// textInputAction: TextInputAction.next, + /// onFieldSubmitted: _handleSubmitted, + /// controller: _controller1, + /// decoration: InputDecoration(border: OutlineInputBorder()), + /// ), + /// ), + /// Padding( + /// padding: const EdgeInsets.all(8.0), + /// child: TextFormField( + /// textInputAction: TextInputAction.next, + /// onFieldSubmitted: _handleSubmitted, + /// controller: _controller2, + /// decoration: InputDecoration(border: OutlineInputBorder()), + /// ), + /// ), + /// ], + /// ), + /// ), + /// ); + /// } + /// ``` + /// {@end-tool} final ValueChanged onSubmitted; /// Called when the user changes the selection of text (including the cursor @@ -1447,7 +1504,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien _showCaretOnScreen(); if (!_value.selection.isValid) { // Place cursor at the end if the selection is invalid when we receive focus. - widget.controller.selection = TextSelection.collapsed(offset: _value.text.length); + _handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), renderEditable, null); } } else { WidgetsBinding.instance.removeObserver(this); @@ -1500,6 +1557,9 @@ class EditableTextState extends State with AutomaticKeepAliveClien @override void hideToolbar() { + if (_selectionOverlay == null || !_selectionOverlay.toolbarIsVisible) { + return; + } _selectionOverlay?.hide(); } diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart index 572e21886b5..5f00e0610c7 100644 --- a/packages/flutter/lib/src/widgets/focus_manager.dart +++ b/packages/flutter/lib/src/widgets/focus_manager.dart @@ -236,6 +236,7 @@ class FocusAttachment { /// /// class _ColorfulButtonState extends State { /// FocusNode _node; +/// bool _focused = false; /// FocusAttachment _nodeAttachment; /// Color _color = Colors.white; /// @@ -243,9 +244,18 @@ class FocusAttachment { /// void initState() { /// super.initState(); /// _node = FocusNode(debugLabel: 'Button'); +/// _node.addListener(_handleFocusChange); /// _nodeAttachment = _node.attach(context, onKey: _handleKeyPress); /// } /// +/// void _handleFocusChange() { +/// if (_node.hasFocus != _focused) { +/// setState(() { +/// _focused = _node.hasFocus; +/// }); +/// } +/// } +/// /// bool _handleKeyPress(FocusNode node, RawKeyEvent event) { /// if (event is RawKeyDownEvent) { /// print('Focus node ${node.debugLabel} got key event: ${event.logicalKey}'); @@ -274,6 +284,7 @@ class FocusAttachment { /// /// @override /// void dispose() { +/// _node.removeListener(_handleFocusChange); /// // The attachment will automatically be detached in dispose(). /// _node.dispose(); /// super.dispose(); @@ -284,24 +295,20 @@ class FocusAttachment { /// _nodeAttachment.reparent(); /// return GestureDetector( /// onTap: () { -/// if (_node.hasFocus) { -/// setState(() { +/// if (_focused) { /// _node.unfocus(); -/// }); /// } else { -/// setState(() { -/// _node.requestFocus(); -/// }); +/// _node.requestFocus(); /// } /// }, /// child: Center( /// child: Container( /// width: 400, /// height: 100, -/// color: _node.hasFocus ? _color : Colors.white, +/// color: _focused ? _color : Colors.white, /// alignment: Alignment.center, /// child: Text( -/// _node.hasFocus ? "I'm in color! Press R,G,B!" : 'Press to focus'), +/// _focused ? "I'm in color! Press R,G,B!" : 'Press to focus'), /// ), /// ), /// ); diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index 28cea99bcd9..d351a031c8d 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -16,7 +16,7 @@ import 'will_pop_scope.dart'; /// with a context whose ancestor is the [Form], or pass a [GlobalKey] to the /// [Form] constructor and call [GlobalKey.currentState]. /// -/// {@tool snippet --template=stateful_widget_material} +/// {@tool snippet --template=stateful_widget_scaffold} /// This example shows a [Form] with one [TextFormField] and a [RaisedButton]. A /// [GlobalKey] is used here to identify the [Form] and validate input. /// diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index 5ec919d1b85..78da4db41fa 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -22,6 +22,7 @@ import 'transitions.dart'; // @override // MyWidgetState createState() => MyWidgetState(); // } +// void setState(VoidCallback fn) { } /// An interpolation between two [BoxConstraints]. /// @@ -231,7 +232,6 @@ abstract class ImplicitlyAnimatedWidget extends StatefulWidget { Key key, this.curve = Curves.linear, @required this.duration, - this.reverseDuration, }) : assert(curve != null), assert(duration != null), super(key: key); @@ -242,12 +242,6 @@ abstract class ImplicitlyAnimatedWidget extends StatefulWidget { /// The duration over which to animate the parameters of this container. final Duration duration; - /// The duration over which to animate the parameters of this container when - /// the animation is going in the reverse direction. - /// - /// Defaults to [duration] if not specified. - final Duration reverseDuration; - @override ImplicitlyAnimatedWidgetState createState(); @@ -255,7 +249,6 @@ abstract class ImplicitlyAnimatedWidget extends StatefulWidget { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(IntProperty('duration', duration.inMilliseconds, unit: 'ms')); - properties.add(IntProperty('reverseDuration', reverseDuration?.inMilliseconds, unit: 'ms', defaultValue: null)); } } @@ -316,7 +309,6 @@ abstract class ImplicitlyAnimatedWidgetState super.initState(); _controller = AnimationController( duration: widget.duration, - reverseDuration: widget.reverseDuration, debugLabel: kDebugMode ? '${widget.toStringShort()}' : null, vsync: this, ); @@ -331,7 +323,6 @@ abstract class ImplicitlyAnimatedWidgetState if (widget.curve != oldWidget.curve) _updateCurve(); _controller.duration = widget.duration; - _controller.reverseDuration = widget.reverseDuration; if (_constructTweens()) { forEachTween((Tween tween, dynamic targetValue, TweenConstructor constructor) { _updateTween(tween, targetValue); @@ -521,10 +512,43 @@ abstract class AnimatedWidgetBaseState exten /// /// {@youtube 560 315 https://www.youtube.com/watch?v=yI-8QHpGIP4} /// -/// Here's an illustration of what using this widget looks like, using a [curve] -/// of [Curves.fastOutSlowIn]. +/// Here's an illustration (implemented below) of what using this widget looks +/// like, using a [curve] of [Curves.fastOutSlowIn]. /// {@animation 250 266 https://flutter.github.io/assets-for-api-docs/assets/widgets/animated_container.mp4} /// +/// {@tool snippet --template=stateful_widget_scaffold} +/// +/// The following example (depicted above) transitions an AnimatedContainer +/// between two states. It adjusts the [height], [width], [color], and +/// [alignment] properties when tapped. +/// +/// ```dart +/// bool selected = false; +/// +/// @override +/// Widget build(BuildContext context) { +/// return GestureDetector( +/// onTap: () { +/// setState(() { +/// selected = !selected; +/// }); +/// }, +/// child: Center( +/// child: AnimatedContainer( +/// width: selected ? 200.0 : 100.0, +/// height: selected ? 100.0 : 200.0, +/// color: selected ? Colors.red : Colors.blue, +/// alignment: selected ? Alignment.center : AlignmentDirectional.topCenter, +/// duration: Duration(seconds: 2), +/// curve: Curves.fastOutSlowIn, +/// child: FlutterLogo(size: 75), +/// ), +/// ), +/// ); +/// } +/// ``` +/// {@end-tool} +/// /// See also: /// /// * [AnimatedPadding], which is a subset of this widget that only @@ -556,7 +580,6 @@ class AnimatedContainer extends ImplicitlyAnimatedWidget { this.child, Curve curve = Curves.linear, @required Duration duration, - Duration reverseDuration, }) : assert(margin == null || margin.isNonNegative), assert(padding == null || padding.isNonNegative), assert(decoration == null || decoration.debugAssertIsValid()), @@ -571,7 +594,7 @@ class AnimatedContainer extends ImplicitlyAnimatedWidget { ? constraints?.tighten(width: width, height: height) ?? BoxConstraints.tightFor(width: width, height: height) : constraints, - super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration); + super(key: key, curve: curve, duration: duration); /// The [child] contained by the container. /// @@ -713,10 +736,9 @@ class AnimatedPadding extends ImplicitlyAnimatedWidget { this.child, Curve curve = Curves.linear, @required Duration duration, - Duration reverseDuration, }) : assert(padding != null), assert(padding.isNonNegative), - super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration); + super(key: key, curve: curve, duration: duration); /// The amount of space by which to inset the child. final EdgeInsetsGeometry padding; @@ -785,9 +807,8 @@ class AnimatedAlign extends ImplicitlyAnimatedWidget { this.child, Curve curve = Curves.linear, @required Duration duration, - Duration reverseDuration, }) : assert(alignment != null), - super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration); + super(key: key, curve: curve, duration: duration); /// How to align the child. /// @@ -886,10 +907,9 @@ class AnimatedPositioned extends ImplicitlyAnimatedWidget { this.height, Curve curve = Curves.linear, @required Duration duration, - Duration reverseDuration, }) : assert(left == null || right == null || width == null), assert(top == null || bottom == null || height == null), - super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration); + super(key: key, curve: curve, duration: duration); /// Creates a widget that animates the rectangle it occupies implicitly. /// @@ -900,14 +920,13 @@ class AnimatedPositioned extends ImplicitlyAnimatedWidget { Rect rect, Curve curve = Curves.linear, @required Duration duration, - Duration reverseDuration, }) : left = rect.left, top = rect.top, width = rect.width, height = rect.height, right = null, bottom = null, - super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration); + super(key: key, curve: curve, duration: duration); /// The widget below this widget in the tree. /// @@ -1039,10 +1058,9 @@ class AnimatedPositionedDirectional extends ImplicitlyAnimatedWidget { this.height, Curve curve = Curves.linear, @required Duration duration, - Duration reverseDuration, }) : assert(start == null || end == null || width == null), assert(top == null || bottom == null || height == null), - super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration); + super(key: key, curve: curve, duration: duration); /// The widget below this widget in the tree. /// @@ -1194,9 +1212,8 @@ class AnimatedOpacity extends ImplicitlyAnimatedWidget { @required this.opacity, Curve curve = Curves.linear, @required Duration duration, - Duration reverseDuration, }) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0), - super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration); + super(key: key, curve: curve, duration: duration); /// The widget below this widget in the tree. /// @@ -1270,13 +1287,12 @@ class AnimatedDefaultTextStyle extends ImplicitlyAnimatedWidget { this.maxLines, Curve curve = Curves.linear, @required Duration duration, - Duration reverseDuration, }) : assert(style != null), assert(child != null), assert(softWrap != null), assert(overflow != null), assert(maxLines == null || maxLines > 0), - super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration); + super(key: key, curve: curve, duration: duration); /// The widget below this widget in the tree. /// @@ -1386,7 +1402,6 @@ class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget { this.animateShadowColor = true, Curve curve = Curves.linear, @required Duration duration, - Duration reverseDuration, }) : assert(child != null), assert(shape != null), assert(clipBehavior != null), @@ -1396,7 +1411,7 @@ class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget { assert(shadowColor != null), assert(animateColor != null), assert(animateShadowColor != null), - super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration); + super(key: key, curve: curve, duration: duration); /// The widget below this widget in the tree. /// diff --git a/packages/flutter/lib/src/widgets/sliver_persistent_header.dart b/packages/flutter/lib/src/widgets/sliver_persistent_header.dart index be4f5f18744..1547c3e2f30 100644 --- a/packages/flutter/lib/src/widgets/sliver_persistent_header.dart +++ b/packages/flutter/lib/src/widgets/sliver_persistent_header.dart @@ -284,10 +284,7 @@ class _SliverScrollingPersistentHeader extends _SliverPersistentHeaderRenderObje } } -// This class exists to work around https://github.com/dart-lang/sdk/issues/31543 -abstract class _RenderSliverScrollingPersistentHeader extends RenderSliverScrollingPersistentHeader { } - -class _RenderSliverScrollingPersistentHeaderForWidgets extends _RenderSliverScrollingPersistentHeader +class _RenderSliverScrollingPersistentHeaderForWidgets extends RenderSliverScrollingPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin { } class _SliverPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget { @@ -302,10 +299,7 @@ class _SliverPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectW } } -// This class exists to work around https://github.com/dart-lang/sdk/issues/31543 -abstract class _RenderSliverPinnedPersistentHeader extends RenderSliverPinnedPersistentHeader { } - -class _RenderSliverPinnedPersistentHeaderForWidgets extends _RenderSliverPinnedPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin { } +class _RenderSliverPinnedPersistentHeaderForWidgets extends RenderSliverPinnedPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin { } class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget { const _SliverFloatingPersistentHeader({ @@ -315,10 +309,7 @@ class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjec @override _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) { - // Not passing this snapConfiguration as a constructor parameter to avoid the - // additional layers added due to https://github.com/dart-lang/sdk/issues/31543 - return _RenderSliverFloatingPersistentHeaderForWidgets() - ..snapConfiguration = delegate.snapConfiguration; + return _RenderSliverFloatingPersistentHeaderForWidgets(snapConfiguration: delegate.snapConfiguration); } @override @@ -327,10 +318,12 @@ class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjec } } -// This class exists to work around https://github.com/dart-lang/sdk/issues/31543 -abstract class _RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPinnedPersistentHeader { } - -class _RenderSliverFloatingPinnedPersistentHeaderForWidgets extends _RenderSliverFloatingPinnedPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin { } +class _RenderSliverFloatingPinnedPersistentHeaderForWidgets extends RenderSliverFloatingPinnedPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin { + _RenderSliverFloatingPinnedPersistentHeaderForWidgets({ + RenderBox child, + FloatingHeaderSnapConfiguration snapConfiguration, + }) : super(child: child, snapConfiguration: snapConfiguration); +} class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget { const _SliverFloatingPinnedPersistentHeader({ @@ -340,10 +333,7 @@ class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRende @override _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) { - // Not passing this snapConfiguration as a constructor parameter to avoid the - // additional layers added due to https://github.com/dart-lang/sdk/issues/31543 - return _RenderSliverFloatingPinnedPersistentHeaderForWidgets() - ..snapConfiguration = delegate.snapConfiguration; + return _RenderSliverFloatingPinnedPersistentHeaderForWidgets(snapConfiguration: delegate.snapConfiguration); } @override @@ -352,7 +342,9 @@ class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRende } } -// This class exists to work around https://github.com/dart-lang/sdk/issues/31543 -abstract class _RenderSliverFloatingPersistentHeader extends RenderSliverFloatingPersistentHeader { } - -class _RenderSliverFloatingPersistentHeaderForWidgets extends _RenderSliverFloatingPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin { } +class _RenderSliverFloatingPersistentHeaderForWidgets extends RenderSliverFloatingPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin { + _RenderSliverFloatingPersistentHeaderForWidgets({ + RenderBox child, + FloatingHeaderSnapConfiguration snapConfiguration, + }) : super(child: child, snapConfiguration: snapConfiguration); +} diff --git a/packages/flutter/lib/src/widgets/table.dart b/packages/flutter/lib/src/widgets/table.dart index d5f9564f13a..7f823fc144a 100644 --- a/packages/flutter/lib/src/widgets/table.dart +++ b/packages/flutter/lib/src/widgets/table.dart @@ -250,13 +250,9 @@ class _TableElement extends RenderObjectElement { List<_TableElementRow> _children = const<_TableElementRow>[]; - bool _debugWillReattachChildren = false; - @override void mount(Element parent, dynamic newSlot) { super.mount(parent, newSlot); - assert(!_debugWillReattachChildren); - assert(() { _debugWillReattachChildren = true; return true; }()); _children = widget.children.map<_TableElementRow>((TableRow row) { return _TableElementRow( key: row.key, @@ -266,32 +262,20 @@ class _TableElement extends RenderObjectElement { }).toList(growable: false), ); }).toList(growable: false); - assert(() { _debugWillReattachChildren = false; return true; }()); _updateRenderObjectChildren(); } @override void insertChildRenderObject(RenderObject child, Element slot) { - assert(_debugWillReattachChildren); renderObject.setupParentData(child); } @override void moveChildRenderObject(RenderObject child, dynamic slot) { - assert(_debugWillReattachChildren); } @override void removeChildRenderObject(RenderObject child) { - assert(() { - if (_debugWillReattachChildren) - return true; - for (Element forgottenChild in _forgottenChildren) { - if (forgottenChild.renderObject == child) - return true; - } - return false; - }()); final TableCellParentData childParentData = child.parentData; renderObject.setChild(childParentData.x, childParentData.y, null); } @@ -300,8 +284,6 @@ class _TableElement extends RenderObjectElement { @override void update(Table newWidget) { - assert(!_debugWillReattachChildren); - assert(() { _debugWillReattachChildren = true; return true; }()); final Map> oldKeyedRows = >{}; for (_TableElementRow row in _children) { if (row.key != null) { @@ -330,7 +312,7 @@ class _TableElement extends RenderObjectElement { updateChildren(oldUnkeyedRows.current.children, const [], forgottenChildren: _forgottenChildren); for (List oldChildren in oldKeyedRows.values.where((List list) => !taken.contains(list))) updateChildren(oldChildren, const [], forgottenChildren: _forgottenChildren); - assert(() { _debugWillReattachChildren = false; return true; }()); + _children = newChildren; _updateRenderObjectChildren(); _forgottenChildren.clear(); diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index ea572c9f0a2..af17c6c1205 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -813,6 +813,318 @@ class _TextSelectionHandleOverlayState } } +/// Delegate interface for the [TextSelectionGestureDetectorBuilder]. +/// +/// The interface is usually implemented by textfield implementations wrapping +/// [EditableText], that use a [TextSelectionGestureDetectorBuilder] to build a +/// [TextSelectionGestureDetector] for their [EditableText]. The delegate provides +/// the builder with information about the current state of the textfield. +/// Based on these information, the builder adds the correct gesture handlers +/// to the gesture detector. +/// +/// See also: +/// +/// * [TextField], which implements this delegate for the Material textfield. +/// * [CupertinoTextField], which implements this delegate for the Cupertino textfield. +abstract class TextSelectionGestureDetectorBuilderDelegate { + /// [GlobalKey] to the [EditableText] for which the + /// [TextSelectionGestureDetectorBuilder] will build a [TextSelectionGestureDetector]. + GlobalKey get editableTextKey; + + /// Whether the textfield should respond to force presses. + bool get forcePressEnabled; + + /// Whether the user may select text in the textfield. + bool get selectionEnabled; +} + +/// Builds a [TextSelectionGestureDetector] to wrap an [EditableText]. +/// +/// The class implements sensible defaults for many user interactions +/// with an [EditableText] (see the documentation of the various gesture handler +/// methods, e.g. [onTapDown], [onFrocePress], etc.). Subclasses of +/// [EditableTextSelectionHandlesProvider] can change the behavior performed in +/// responds to these gesture events by overriding the corresponding handler +/// methods of this class. +/// +/// The resulting [TextSelectionGestureDetector] to wrap an [EditableText] is +/// obtained by calling [buildGestureDetector]. +/// +/// See also: +/// +/// * [TextField], which uses a subclass to implement the Material-specific +/// gesture logic of an [EditableText]. +/// * [CupertinoTextField], which uses a subclass to implement the +/// Cupertino-specific gesture logic of an [EditableText]. +class TextSelectionGestureDetectorBuilder { + /// Creates a [TextSelectionGestureDetectorBuilder]. + /// + /// The [delegate] must not be null. + TextSelectionGestureDetectorBuilder({ + @required this.delegate, + }) : assert(delegate != null); + + /// The delegate for this [TextSelectionGestureDetectorBuilder]. + /// + /// The delegate provides the builder with information about what actions can + /// currently be performed on the textfield. Based on this, the builder adds + /// the correct gesture handlers to the gesture detector. + @protected + final TextSelectionGestureDetectorBuilderDelegate delegate; + + /// Whether to show the selection tool bar. + /// + /// It is based on the signal source when a [onTapDown] is called. This getter + /// will return true if current [onTapDown] event is triggered by a touch or + /// a stylus. + bool get shouldShowSelectionToolbar => _shouldShowSelectionToolbar; + bool _shouldShowSelectionToolbar = true; + + /// The [State] of the [EditableText] for which the builder will provide a + /// [TextSelectionGestureDetector]. + @protected + EditableTextState get editableText => delegate.editableTextKey.currentState; + + /// The [RenderObject] of the [EditableText] for which the builder will + /// provide a [TextSelectionGestureDetector]. + @protected + RenderEditable get renderEditable => editableText.renderEditable; + + /// Handler for [TextSelectionGestureDetector.onTapDown]. + /// + /// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets + /// [shouldShowSelectionToolbar] to true if the tap was initiated by a finger or stylus. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onTapDown], which triggers this callback. + @protected + void onTapDown(TapDownDetails details) { + renderEditable.handleTapDown(details); + // The selection overlay should only be shown when the user is interacting + // through a touch screen (via either a finger or a stylus). A mouse shouldn't + // trigger the selection overlay. + // For backwards-compatibility, we treat a null kind the same as touch. + final PointerDeviceKind kind = details.kind; + _shouldShowSelectionToolbar = kind == null + || kind == PointerDeviceKind.touch + || kind == PointerDeviceKind.stylus; + } + + /// Handler for [TextSelectionGestureDetector.onForcePressStart]. + /// + /// By default, it selects the word at the position of the force press, + /// if selection is enabled. + /// + /// This callback is only applicable when force press is enabled. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onForcePressStart], which triggers this + /// callback. + @protected + void onForcePressStart(ForcePressDetails details) { + assert(delegate.forcePressEnabled); + _shouldShowSelectionToolbar = true; + if (delegate.selectionEnabled) { + renderEditable.selectWordsInRange( + from: details.globalPosition, + cause: SelectionChangedCause.forcePress, + ); + } + } + + /// Handler for [TextSelectionGestureDetector.onForcePressEnd]. + /// + /// By default, it selects words in the range specified in [details] and shows + /// tool bar if it is necessary. + /// + /// This callback is only applicable when force press is enabled. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onForcePressEnd], which triggers this + /// callback. + @protected + void onForcePressEnd(ForcePressDetails details) { + assert(delegate.forcePressEnabled); + renderEditable.selectWordsInRange( + from: details.globalPosition, + cause: SelectionChangedCause.forcePress, + ); + if (shouldShowSelectionToolbar) + editableText.showToolbar(); + } + + /// Handler for [TextSelectionGestureDetector.onSingleTapUp]. + /// + /// By default, it selects word edge if selection is enabled. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onSingleTapUp], which triggers + /// this callback. + @protected + void onSingleTapUp(TapUpDetails details) { + if (delegate.selectionEnabled) { + renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); + } + } + + /// Handler for [TextSelectionGestureDetector.onSingleTapCancel]. + /// + /// By default, it services as place holder to enable subclass override. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onSingleTapCancel], which triggers + /// this callback. + @protected + void onSingleTapCancel() {/* Subclass should override this method if needed. */} + + /// Handler for [TextSelectionGestureDetector.onSingleLongTapStart]. + /// + /// By default, it selects text position specified in [details] if selection + /// is enabled. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onSingleLongTapStart], which triggers + /// this callback. + @protected + void onSingleLongTapStart(LongPressStartDetails details) { + if (delegate.selectionEnabled) { + renderEditable.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.longPress, + ); + } + } + + /// Handler for [TextSelectionGestureDetector.onSingleLongTapMoveUpdate]. + /// + /// By default, it updates the selection location specified in [details] if + /// selection is enabled. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onSingleLongTapMoveUpdate], which + /// triggers this callback. + @protected + void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { + if (delegate.selectionEnabled) { + renderEditable.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.longPress, + ); + } + } + + /// Handler for [TextSelectionGestureDetector.onSingleLongTapEnd]. + /// + /// By default, it shows tool bar if necessary. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onSingleLongTapEnd], which triggers this + /// callback. + @protected + void onSingleLongTapEnd(LongPressEndDetails details) { + if (shouldShowSelectionToolbar) + editableText.showToolbar(); + } + + /// Handler for [TextSelectionGestureDetector.onDoubleTapDown]. + /// + /// By default, it selects a word through [renderEditable.selectWord] if + /// selectionEnabled and shows tool bar if necessary. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this + /// callback. + @protected + void onDoubleTapDown(TapDownDetails details) { + if (delegate.selectionEnabled) { + renderEditable.selectWord(cause: SelectionChangedCause.tap); + if (shouldShowSelectionToolbar) + editableText.showToolbar(); + } + } + + /// Handler for [TextSelectionGestureDetector.onDragSelectionStart]. + /// + /// By default, it selects a text position specified in [details]. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onDragSelectionStart], which triggers + /// this callback. + @protected + void onDragSelectionStart(DragStartDetails details) { + renderEditable.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.drag, + ); + } + + /// Handler for [TextSelectionGestureDetector.onDragSelectionUpdate]. + /// + /// By default, it updates the selection location specified in [details]. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onDragSelectionUpdate], which triggers + /// this callback./lib/src/material/text_field.dart + @protected + void onDragSelectionUpdate(DragStartDetails startDetails, DragUpdateDetails updateDetails) { + renderEditable.selectPositionAt( + from: startDetails.globalPosition, + to: updateDetails.globalPosition, + cause: SelectionChangedCause.drag, + ); + } + + /// Handler for [TextSelectionGestureDetector.onDragSelectionEnd]. + /// + /// By default, it services as place holder to enable subclass override. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onDragSelectionEnd], which triggers this + /// callback. + @protected + void onDragSelectionEnd(DragEndDetails details) {/* Subclass should override this method if needed. */} + + /// Returns a [TextSelectionGestureDetector] configured with the handlers + /// provided by this builder. + /// + /// The [child] or its subtree should contain [EditableText]. + Widget buildGestureDetector({ + Key key, + HitTestBehavior behavior, + Widget child + }) { + return TextSelectionGestureDetector( + key: key, + onTapDown: onTapDown, + onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null, + onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null, + onSingleTapUp: onSingleTapUp, + onSingleTapCancel: onSingleTapCancel, + onSingleLongTapStart: onSingleLongTapStart, + onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate, + onSingleLongTapEnd: onSingleLongTapEnd, + onDoubleTapDown: onDoubleTapDown, + onDragSelectionStart: onDragSelectionStart, + onDragSelectionUpdate: onDragSelectionUpdate, + onDragSelectionEnd: onDragSelectionEnd, + behavior: behavior, + child: child, + ); + } +} + /// A gesture detector to respond to non-exclusive event chains for a text field. /// /// An ordinary [GestureDetector] configured to handle events like tap and diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index d0871cfa1ae..5c8e006e4c4 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -27,6 +27,7 @@ export 'src/widgets/banner.dart'; export 'src/widgets/basic.dart'; export 'src/widgets/binding.dart'; export 'src/widgets/bottom_navigation_bar_item.dart'; +export 'src/widgets/color_filter.dart'; export 'src/widgets/container.dart'; export 'src/widgets/debug.dart'; export 'src/widgets/dismissible.dart'; diff --git a/packages/flutter/test/cupertino/date_picker_test.dart b/packages/flutter/test/cupertino/date_picker_test.dart index ae902120384..0b8908f0f09 100644 --- a/packages/flutter/test/cupertino/date_picker_test.dart +++ b/packages/flutter/test/cupertino/date_picker_test.dart @@ -847,7 +847,6 @@ void main() { 'date_picker_test.datetime.initial.png', version: 1, ), - skip: !isLinux ); // Slightly drag the hour component to make the current hour off-center. @@ -860,7 +859,6 @@ void main() { 'date_picker_test.datetime.drag.png', version: 1, ), - skip: !isLinux ); }); }); diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart index 96afba335d9..56afa4a7837 100644 --- a/packages/flutter/test/cupertino/nav_bar_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_test.dart @@ -805,9 +805,6 @@ void main() { ), ); }, - // TODO(xster): remove once https://github.com/flutter/flutter/issues/17483 - // is fixed. - skip: !isLinux, ); testWidgets( @@ -842,10 +839,7 @@ void main() { ), ); }, - // TODO(xster): remove once https://github.com/flutter/flutter/issues/17483 - // is fixed. - skip: !isLinux, - ); + ); testWidgets('NavBar draws a light system bar for a dark background', (WidgetTester tester) async { diff --git a/packages/flutter/test/cupertino/segmented_control_test.dart b/packages/flutter/test/cupertino/segmented_control_test.dart index 4cacef60242..71556c81715 100644 --- a/packages/flutter/test/cupertino/segmented_control_test.dart +++ b/packages/flutter/test/cupertino/segmented_control_test.dart @@ -1417,7 +1417,7 @@ void main() { version: 0, ), ); - }, skip: !isLinux); + }); testWidgets('Golden Test Pressed State', (WidgetTester tester) async { final Map children = {}; @@ -1458,5 +1458,5 @@ void main() { version: 0, ), ); - }, skip: !isLinux); + }); } diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index d0c2cc195aa..dcdc751f7d8 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -481,7 +481,7 @@ void main() { version: 2, ), ); - }, skip: !isLinux); + }); testWidgets('Cupertino cursor iOS golden', (WidgetTester tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; @@ -514,7 +514,7 @@ void main() { version: 2, ), ); - }, skip: !isLinux); + }); testWidgets( 'can control text content via controller', @@ -2901,7 +2901,6 @@ void main() { 'text_field_test.disabled.png', version: 0, ), - skip: !isLinux, ); }); @@ -3650,5 +3649,31 @@ void main() { expect(tester.getTopLeft(find.byType(EditableText)).dy, closeTo(329.0, .0001)); }); }); + + testWidgets( + 'Long press on an autofocused field shows the selection menu', + (WidgetTester tester) async { + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: ConstrainedBox( + constraints: BoxConstraints.loose(const Size(200, 200)), + child: const CupertinoTextField( + autofocus: true, + ), + ), + ), + ), + ); + // This extra pump allows the selection set by autofocus to propagate to + // the RenderEditable. + await tester.pump(); + + // Long press shows the selection menu. + await tester.longPressAt(textOffsetToPosition(tester, 0)); + await tester.pump(); + expect(find.text('Paste'), findsOneWidget); + }, + ); }); } diff --git a/packages/flutter/test/material/app_test.dart b/packages/flutter/test/material/app_test.dart index 56c9fa7659d..4135b2827ea 100644 --- a/packages/flutter/test/material/app_test.dart +++ b/packages/flutter/test/material/app_test.dart @@ -447,7 +447,99 @@ void main() { expect(find.text('Select All'), findsOneWidget); }); - testWidgets('MaterialApp uses regular theme when platformBrightness is light', (WidgetTester tester) async { + testWidgets('MaterialApp uses regular theme when themeMode is light', (WidgetTester tester) async { + // Mock the Window to explicitly report a light platformBrightness. + tester.binding.window.platformBrightnessTestValue = Brightness.light; + + ThemeData appliedTheme; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + brightness: Brightness.light + ), + darkTheme: ThemeData( + brightness: Brightness.dark, + ), + themeMode: ThemeMode.light, + home: Builder( + builder: (BuildContext context) { + appliedTheme = Theme.of(context); + return const SizedBox(); + }, + ), + ), + ); + expect(appliedTheme.brightness, Brightness.light); + + // Mock the Window to explicitly report a dark platformBrightness. + tester.binding.window.platformBrightnessTestValue = Brightness.dark; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + brightness: Brightness.light + ), + darkTheme: ThemeData( + brightness: Brightness.dark, + ), + themeMode: ThemeMode.light, + home: Builder( + builder: (BuildContext context) { + appliedTheme = Theme.of(context); + return const SizedBox(); + }, + ), + ), + ); + expect(appliedTheme.brightness, Brightness.light); + }); + + testWidgets('MaterialApp uses darkTheme when themeMode is dark', (WidgetTester tester) async { + // Mock the Window to explicitly report a light platformBrightness. + tester.binding.window.platformBrightnessTestValue = Brightness.light; + + ThemeData appliedTheme; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + brightness: Brightness.light + ), + darkTheme: ThemeData( + brightness: Brightness.dark, + ), + themeMode: ThemeMode.dark, + home: Builder( + builder: (BuildContext context) { + appliedTheme = Theme.of(context); + return const SizedBox(); + }, + ), + ), + ); + expect(appliedTheme.brightness, Brightness.dark); + + // Mock the Window to explicitly report a dark platformBrightness. + tester.binding.window.platformBrightnessTestValue = Brightness.dark; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + brightness: Brightness.light + ), + darkTheme: ThemeData( + brightness: Brightness.dark, + ), + themeMode: ThemeMode.dark, + home: Builder( + builder: (BuildContext context) { + appliedTheme = Theme.of(context); + return const SizedBox(); + }, + ), + ), + ); + expect(appliedTheme.brightness, Brightness.dark); + }); + + testWidgets('MaterialApp uses regular theme when themeMode is system and platformBrightness is light', (WidgetTester tester) async { // Mock the Window to explicitly report a light platformBrightness. final TestWidgetsFlutterBinding binding = tester.binding; binding.window.platformBrightnessTestValue = Brightness.light; @@ -462,6 +554,7 @@ void main() { darkTheme: ThemeData( brightness: Brightness.dark, ), + themeMode: ThemeMode.system, home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); @@ -474,6 +567,31 @@ void main() { expect(appliedTheme.brightness, Brightness.light); }); + testWidgets('MaterialApp uses darkTheme when themeMode is system and platformBrightness is dark', (WidgetTester tester) async { + // Mock the Window to explicitly report a dark platformBrightness. + tester.binding.window.platformBrightnessTestValue = Brightness.dark; + + ThemeData appliedTheme; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + brightness: Brightness.light + ), + darkTheme: ThemeData( + brightness: Brightness.dark, + ), + themeMode: ThemeMode.system, + home: Builder( + builder: (BuildContext context) { + appliedTheme = Theme.of(context); + return const SizedBox(); + }, + ), + ), + ); + expect(appliedTheme.brightness, Brightness.dark); + }); + testWidgets('MaterialApp uses light theme when platformBrightness is dark but no dark theme is provided', (WidgetTester tester) async { // Mock the Window to explicitly report a dark platformBrightness. final TestWidgetsFlutterBinding binding = tester.binding; diff --git a/packages/flutter/test/material/bottom_app_bar_test.dart b/packages/flutter/test/material/bottom_app_bar_test.dart index 4a0739836a5..7f1b3dcf6aa 100644 --- a/packages/flutter/test/material/bottom_app_bar_test.dart +++ b/packages/flutter/test/material/bottom_app_bar_test.dart @@ -75,7 +75,6 @@ void main() { 'bottom_app_bar.custom_shape.1.png', version: null, ), - skip: !isLinux, ); await pump(FloatingActionButtonLocation.centerDocked); await tester.pumpAndSettle(); @@ -85,7 +84,6 @@ void main() { 'bottom_app_bar.custom_shape.2.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); diff --git a/packages/flutter/test/material/bottom_app_bar_theme_test.dart b/packages/flutter/test/material/bottom_app_bar_theme_test.dart index 4bbb597cdb1..3b4cd47815e 100644 --- a/packages/flutter/test/material/bottom_app_bar_theme_test.dart +++ b/packages/flutter/test/material/bottom_app_bar_theme_test.dart @@ -84,7 +84,6 @@ void main() { 'bottom_app_bar_theme.custom_shape.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); diff --git a/packages/flutter/test/material/bottom_navigation_bar_test.dart b/packages/flutter/test/material/bottom_navigation_bar_test.dart index e8b277c1491..d8bc3a763f1 100644 --- a/packages/flutter/test/material/bottom_navigation_bar_test.dart +++ b/packages/flutter/test/material/bottom_navigation_bar_test.dart @@ -1438,7 +1438,6 @@ void main() { 'bottom_navigation_bar.shifting_transition.$pump.png', version: 2, ), - skip: !isLinux, ); } }, skip: isBrowser); diff --git a/packages/flutter/test/material/bottom_sheet_test.dart b/packages/flutter/test/material/bottom_sheet_test.dart index 15cca8e8ac1..f0cf7e6a054 100644 --- a/packages/flutter/test/material/bottom_sheet_test.dart +++ b/packages/flutter/test/material/bottom_sheet_test.dart @@ -361,4 +361,91 @@ void main() { ), ignoreTransform: true, ignoreRect: true, ignoreId: true)); semantics.dispose(); }); + + testWidgets('showModalBottomSheet does not use root Navigator by default', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Navigator(onGenerateRoute: (RouteSettings settings) => MaterialPageRoute(builder: (_) { + return const _TestPage(); + })), + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.ac_unit), + title: Text('Item 1'), + ), + BottomNavigationBarItem( + icon: Icon(Icons.style), + title: Text('Item 2'), + ) + ], + ), + ), + )); + + await tester.tap(find.text('Show bottom sheet')); + await tester.pumpAndSettle(); + + // Bottom sheet is displayed in correct position within the inner navigator + // and above the BottomNavigationBar. + expect(tester.getBottomLeft(find.byType(BottomSheet)).dy, 544.0); + }); + + testWidgets('showModalBottomSheet uses root Navigator when specified', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Navigator(onGenerateRoute: (RouteSettings settings) => MaterialPageRoute(builder: (_) { + return const _TestPage(useRootNavigator: true); + })), + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.ac_unit), + title: Text('Item 1'), + ), + BottomNavigationBarItem( + icon: Icon(Icons.style), + title: Text('Item 2'), + ) + ], + ), + ), + )); + + await tester.tap(find.text('Show bottom sheet')); + await tester.pumpAndSettle(); + + // Bottom sheet is displayed in correct position above all content including + // the BottomNavigationBar. + expect(tester.getBottomLeft(find.byType(BottomSheet)).dy, 600.0); + }); +} + +class _TestPage extends StatelessWidget { + const _TestPage({Key key, this.useRootNavigator}) : super(key: key); + + final bool useRootNavigator; + + @override + Widget build(BuildContext context) { + return Center( + child: FlatButton( + child: const Text('Show bottom sheet'), + onPressed: () { + if (useRootNavigator != null) { + showModalBottomSheet( + useRootNavigator: useRootNavigator, + context: context, + builder: (_) => const Text('Modal bottom sheet'), + ); + } else { + showModalBottomSheet( + context: context, + builder: (_) => const Text('Modal bottom sheet'), + ); + } + } + ), + ); + } } diff --git a/packages/flutter/test/material/card_theme_test.dart b/packages/flutter/test/material/card_theme_test.dart index 8dedd6c21aa..abdf8cc6db5 100644 --- a/packages/flutter/test/material/card_theme_test.dart +++ b/packages/flutter/test/material/card_theme_test.dart @@ -141,7 +141,6 @@ void main() { 'card_theme.custom_shape.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); } diff --git a/packages/flutter/test/material/dialog_theme_test.dart b/packages/flutter/test/material/dialog_theme_test.dart index 64378abfc72..133ecc1da71 100644 --- a/packages/flutter/test/material/dialog_theme_test.dart +++ b/packages/flutter/test/material/dialog_theme_test.dart @@ -134,7 +134,6 @@ void main() { 'dialog_theme.dialog_with_custom_border.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); diff --git a/packages/flutter/test/material/dropdown_test.dart b/packages/flutter/test/material/dropdown_test.dart index b16d9bcfec6..03c480ce187 100644 --- a/packages/flutter/test/material/dropdown_test.dart +++ b/packages/flutter/test/material/dropdown_test.dart @@ -144,7 +144,6 @@ void main() { 'dropdown_test.default.png', version: 0, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -160,7 +159,6 @@ void main() { 'dropdown_test.expanded.png', version: 0, ), - skip: !isLinux, ); }, skip: isBrowser); diff --git a/packages/flutter/test/material/expand_icon_test.dart b/packages/flutter/test/material/expand_icon_test.dart index 16dc6de25d0..9cf2b6200e0 100644 --- a/packages/flutter/test/material/expand_icon_test.dart +++ b/packages/flutter/test/material/expand_icon_test.dart @@ -92,7 +92,7 @@ void main() { await tester.pumpAndSettle(); iconTheme = tester.firstWidget(find.byType(IconTheme).last); - expect(iconTheme.data.color, equals(Colors.white30)); + expect(iconTheme.data.color, equals(Colors.white38)); }); testWidgets('ExpandIcon test isExpanded does not trigger callback', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/floating_action_button_test.dart b/packages/flutter/test/material/floating_action_button_test.dart index 8b89ba85b93..5be827065d0 100644 --- a/packages/flutter/test/material/floating_action_button_test.dart +++ b/packages/flutter/test/material/floating_action_button_test.dart @@ -740,7 +740,6 @@ void main() { 'floating_action_button_test.clip.png', version: 2, ), - skip: !isLinux, ); }); diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 405182550df..1d35a7d40fd 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -2844,7 +2844,6 @@ void main() { 'input_decorator.outline_icon_label.ltr.png', version: null, ), - skip: !isLinux, ); await tester.pumpWidget(buildFrame(TextDirection.rtl)); @@ -2854,10 +2853,8 @@ void main() { 'input_decorator.outline_icon_label.rtl.png', version: null, ), - skip: !isLinux, ); }, - skip: !isLinux, ); testWidgets('InputDecorator draws and animates hoverColor', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/material_test.dart b/packages/flutter/test/material/material_test.dart index cb5b103c141..dac786582b5 100644 --- a/packages/flutter/test/material/material_test.dart +++ b/packages/flutter/test/material/material_test.dart @@ -620,7 +620,6 @@ void main() { 'material.border_paint_above.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -664,7 +663,6 @@ void main() { 'material.border_paint_below.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); }); diff --git a/packages/flutter/test/material/radio_test.dart b/packages/flutter/test/material/radio_test.dart index 29d8748c64e..bf817a5090e 100644 --- a/packages/flutter/test/material/radio_test.dart +++ b/packages/flutter/test/material/radio_test.dart @@ -280,7 +280,6 @@ void main() { 'radio.ink_ripple.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); } diff --git a/packages/flutter/test/material/tab_bar_theme_test.dart b/packages/flutter/test/material/tab_bar_theme_test.dart index f17d4e08076..bb97c5d19f3 100644 --- a/packages/flutter/test/material/tab_bar_theme_test.dart +++ b/packages/flutter/test/material/tab_bar_theme_test.dart @@ -271,7 +271,6 @@ void main() { 'tab_bar_theme.tab_indicator_size_tab.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -286,7 +285,6 @@ void main() { 'tab_bar_theme.tab_indicator_size_label.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -306,7 +304,6 @@ void main() { 'tab_bar_theme.custom_tab_indicator.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -326,7 +323,6 @@ void main() { 'tab_bar_theme.beveled_rect_indicator.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 0605c0a8527..0f0b0d7c3e6 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -92,19 +92,21 @@ Widget overlayWithEntry(OverlayEntry entry) { } Widget boilerplate({ Widget child }) { - return Localizations( - locale: const Locale('en', 'US'), - delegates: >[ - WidgetsLocalizationsDelegate(), - MaterialLocalizationsDelegate(), - ], - child: Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(size: Size(800.0, 600.0)), - child: Center( - child: Material( - child: child, + return MaterialApp( + home: Localizations( + locale: const Locale('en', 'US'), + delegates: >[ + WidgetsLocalizationsDelegate(), + MaterialLocalizationsDelegate(), + ], + child: Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(size: Size(800.0, 600.0)), + child: Center( + child: Material( + child: child, + ), ), ), ), @@ -414,7 +416,7 @@ void main() { version: 0, ), ); - }, skip: !isLinux); + }); testWidgets('Material cursor iOS golden', (WidgetTester tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; @@ -446,7 +448,7 @@ void main() { version: 0, ), ); - }, skip: !isLinux); + }); testWidgets('text field selection toolbar renders correctly inside opacity', (WidgetTester tester) async { await tester.pumpWidget( @@ -499,7 +501,6 @@ void main() { 'text_field_opacity_test.0.png', version: 2, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -5341,6 +5342,66 @@ void main() { }, ); + testWidgets( + 'A single tap hides the selection menu', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: '', + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: TextField( + controller: controller, + ), + ), + ), + ), + ); + + // Long press shows the selection menu. + await tester.longPress(find.byType(TextField)); + await tester.pump(); + expect(find.text('PASTE'), findsOneWidget); + + // Tap hides the selection menu. + await tester.tap(find.byType(TextField)); + await tester.pump(); + expect(find.text('PASTE'), findsNothing); + }, + ); + + testWidgets( + 'Long press on an autofocused field shows the selection menu', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: '', + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: TextField( + autofocus: true, + controller: controller, + ), + ), + ), + ), + ); + // This extra pump allows the selection set by autofocus to propagate to + // the RenderEditable. + await tester.pump(); + + // Long press shows the selection menu. + expect(find.text('PASTE'), findsNothing); + await tester.longPress(find.byType(TextField)); + await tester.pump(); + expect(find.text('PASTE'), findsOneWidget); + }, + ); + testWidgets( 'double tap hold selects word (iOS)', (WidgetTester tester) async { diff --git a/packages/flutter/test/painting/continous_rectangle_border_test.dart b/packages/flutter/test/painting/continous_rectangle_border_test.dart index 70e4526baf3..f644fb020b0 100644 --- a/packages/flutter/test/painting/continous_rectangle_border_test.dart +++ b/packages/flutter/test/painting/continous_rectangle_border_test.dart @@ -75,7 +75,6 @@ void main() { 'continuous_rectangle_border.golden_test_even_radii.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -100,7 +99,6 @@ void main() { 'continuous_rectangle_border.golden_test_varying_radii.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -122,7 +120,6 @@ void main() { 'continuous_rectangle_border.golden_test_large_radii.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); diff --git a/packages/flutter/test/painting/image_provider_test.dart b/packages/flutter/test/painting/image_provider_test.dart index de62119d4b7..656edfce92c 100644 --- a/packages/flutter/test/painting/image_provider_test.dart +++ b/packages/flutter/test/painting/image_provider_test.dart @@ -44,11 +44,19 @@ void main() { final ImageCache otherCache = ImageCache(); final Uint8List bytes = Uint8List.fromList(kTransparentImage); final MemoryImage imageProvider = MemoryImage(bytes); - otherCache.putIfAbsent(imageProvider, () => imageProvider.load(imageProvider)); + final ImageStreamCompleter cacheStream = otherCache.putIfAbsent( + imageProvider, () => imageProvider.load(imageProvider), + ); final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); final Completer completer = Completer(); - stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) => completer.complete())); - await completer.future; + final Completer cacheCompleter = Completer(); + stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) { + completer.complete(); + })); + cacheStream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) { + cacheCompleter.complete(); + })); + await Future.wait(>[completer.future, cacheCompleter.future]); expect(otherCache.currentSize, 1); expect(imageCache.currentSize, 1); diff --git a/packages/flutter/test/rendering/layers_test.dart b/packages/flutter/test/rendering/layers_test.dart index 8abcf84da5b..0137209973f 100644 --- a/packages/flutter/test/rendering/layers_test.dart +++ b/packages/flutter/test/rendering/layers_test.dart @@ -250,6 +250,15 @@ void main() { }); }); + test('mutating ColorFilterLayer fields triggers needsAddToScene', () { + final ColorFilterLayer layer = ColorFilterLayer( + colorFilter: const ColorFilter.mode(Color(0xFFFF0000), BlendMode.color), + ); + checkNeedsAddToScene(layer, () { + layer.colorFilter = const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color); + }); + }); + test('mutating ShaderMaskLayer fields triggers needsAddToScene', () { const Gradient gradient = RadialGradient(colors: [Color(0x00000000), Color(0x00000001)]); final Shader shader = gradient.createShader(Rect.zero); diff --git a/packages/flutter/test/rendering/localized_fonts_test.dart b/packages/flutter/test/rendering/localized_fonts_test.dart index 025bb5610b0..e269699bb3d 100644 --- a/packages/flutter/test/rendering/localized_fonts_test.dart +++ b/packages/flutter/test/rendering/localized_fonts_test.dart @@ -55,7 +55,6 @@ void main() { ), ); }, - skip: !isLinux, ); testWidgets( @@ -110,7 +109,6 @@ void main() { ), ); }, - skip: !isLinux, ); testWidgets( @@ -157,7 +155,6 @@ void main() { ), ); }, - skip: !isLinux, ); } diff --git a/packages/flutter/test/widgets/backdrop_filter_test.dart b/packages/flutter/test/widgets/backdrop_filter_test.dart index c728f50ba1d..db339be4da2 100644 --- a/packages/flutter/test/widgets/backdrop_filter_test.dart +++ b/packages/flutter/test/widgets/backdrop_filter_test.dart @@ -47,7 +47,6 @@ void main() { 'backdrop_filter_test.cull_rect.png', version: 1, ), - skip: !isLinux, ); }, skip: isBrowser); } diff --git a/packages/flutter/test/widgets/color_filter_test.dart b/packages/flutter/test/widgets/color_filter_test.dart new file mode 100644 index 00000000000..2f09cd15eba --- /dev/null +++ b/packages/flutter/test/widgets/color_filter_test.dart @@ -0,0 +1,69 @@ +// Copyright 2019 The Flutter 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 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; + +void main() { + testWidgets('Color filter - red', (WidgetTester tester) async { + await tester.pumpWidget( + const RepaintBoundary( + child: ColorFiltered( + colorFilter: ColorFilter.mode(Colors.red, BlendMode.color), + child: Placeholder(), + ), + ), + ); + await expectLater( + find.byType(ColorFiltered), + matchesGoldenFile( + 'color_filter_red.png', + version: 1, + ), + skip: !isLinux + ); + }); + + testWidgets('Color filter - sepia', (WidgetTester tester) async { + // TODO(dnfield): This should be const. https://github.com/dart-lang/sdk/issues/37503 + final ColorFilter sepia = ColorFilter.matrix([ + 0.39, 0.769, 0.189, 0, 0, // + 0.349, 0.686, 0.168, 0, 0, // + 0.272, 0.534, 0.131, 0, 0, // + 0, 0, 0, 1, 0, // + ]); + await tester.pumpWidget( + RepaintBoundary( + child: ColorFiltered( + colorFilter: sepia, + child: MaterialApp( + title: 'Flutter Demo', + theme: ThemeData(primarySwatch: Colors.blue), + home: Scaffold( + appBar: AppBar( + title: const Text('Sepia ColorFilter Test'), + ), + body: const Center( + child:Text('Hooray!'), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { }, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ), + ), + ), + ) + ); + await expectLater( + find.byType(ColorFiltered), + matchesGoldenFile( + 'color_filter_sepia.png', + version: 1, + ), + skip: !isLinux + ); + }); +} \ No newline at end of file diff --git a/packages/flutter/test/widgets/editable_text_cursor_test.dart b/packages/flutter/test/widgets/editable_text_cursor_test.dart index 08c2d0fa45e..3c72be430f2 100644 --- a/packages/flutter/test/widgets/editable_text_cursor_test.dart +++ b/packages/flutter/test/widgets/editable_text_cursor_test.dart @@ -95,7 +95,7 @@ void main() { version: 3, ), ); - }, skip: !isLinux); + }); testWidgets('cursor layout has correct radius', (WidgetTester tester) async { final GlobalKey editableTextKey = GlobalKey(); @@ -149,7 +149,7 @@ void main() { version: 3, ), ); - }, skip: !isLinux); + }); testWidgets('Cursor animates on iOS', (WidgetTester tester) async { final Widget widget = MaterialApp( @@ -759,6 +759,5 @@ void main() { ), ); debugDefaultTargetPlatformOverride = null; - }, skip: !isLinux); - + }); } diff --git a/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart b/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart index b0977213bc9..e778c75e59a 100644 --- a/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart +++ b/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart @@ -202,27 +202,29 @@ void main() { final PageController pageController = PageController(initialPage: 1); await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: Material( - child: PageView( - controller: pageController, - children: [ - Container( - color: Colors.red, - ), - Container( - child: TextField( - controller: textController, + MaterialApp( + home: MediaQuery( + data: const MediaQueryData(devicePixelRatio: 1.0), + child: Directionality( + textDirection: TextDirection.ltr, + child: Material( + child: PageView( + controller: pageController, + children: [ + Container( + color: Colors.red, ), - color: Colors.green, - ), - Container( - color: Colors.red, - ), - ], + Container( + child: TextField( + controller: textController, + ), + color: Colors.green, + ), + Container( + color: Colors.red, + ), + ], + ), ), ), ), diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 0f5dce95799..8b66e89150f 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -809,23 +809,25 @@ void main() { return StatefulBuilder( builder: (BuildContext context, StateSetter setter) { setState = setter; - return MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: Material( - child: EditableText( - backgroundCursorColor: Colors.grey, - controller: currentController, - focusNode: focusNode, - style: Typography(platform: TargetPlatform.android) - .black - .subhead, - cursorColor: Colors.blue, - selectionControls: materialTextSelectionControls, - keyboardType: TextInputType.text, - onChanged: (String value) { }, + return MaterialApp( + home: MediaQuery( + data: const MediaQueryData(devicePixelRatio: 1.0), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: EditableText( + backgroundCursorColor: Colors.grey, + controller: currentController, + focusNode: focusNode, + style: Typography(platform: TargetPlatform.android) + .black + .subhead, + cursorColor: Colors.blue, + selectionControls: materialTextSelectionControls, + keyboardType: TextInputType.text, + onChanged: (String value) { }, + ), ), ), ), diff --git a/packages/flutter/test/widgets/form_test.dart b/packages/flutter/test/widgets/form_test.dart index 73cf0307620..33b2e7bc957 100644 --- a/packages/flutter/test/widgets/form_test.dart +++ b/packages/flutter/test/widgets/form_test.dart @@ -11,16 +11,18 @@ void main() { String fieldValue; Widget builder() { - return MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: Material( - child: Form( - key: formKey, - child: TextFormField( - onSaved: (String value) { fieldValue = value; }, + return MaterialApp( + home: MediaQuery( + data: const MediaQueryData(devicePixelRatio: 1.0), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: Form( + key: formKey, + child: TextFormField( + onSaved: (String value) { fieldValue = value; }, + ), ), ), ), @@ -36,7 +38,7 @@ void main() { Future checkText(String testValue) async { await tester.enterText(find.byType(TextFormField), testValue); formKey.currentState.save(); - // pump'ing is unnecessary because callback happens regardless of frames + // Pumping is unnecessary because callback happens regardless of frames. expect(fieldValue, equals(testValue)); } @@ -48,15 +50,17 @@ void main() { String fieldValue; Widget builder() { - return MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: Material( - child: Form( - child: TextField( - onChanged: (String value) { fieldValue = value; }, + return MaterialApp( + home: MediaQuery( + data: const MediaQueryData(devicePixelRatio: 1.0), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: Form( + child: TextField( + onChanged: (String value) { fieldValue = value; }, + ), ), ), ), @@ -84,17 +88,19 @@ void main() { String errorText(String value) => value + '/error'; Widget builder(bool autovalidate) { - return MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: Material( - child: Form( - key: formKey, - autovalidate: autovalidate, - child: TextFormField( - validator: errorText, + return MaterialApp( + home: MediaQuery( + data: const MediaQueryData(devicePixelRatio: 1.0), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: Form( + key: formKey, + autovalidate: autovalidate, + child: TextFormField( + validator: errorText, + ), ), ), ), @@ -138,24 +144,26 @@ void main() { String errorText(String input) => '${fieldKey.currentState.value}/error'; Widget builder() { - return MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: Material( - child: Form( - key: formKey, - autovalidate: true, - child: ListView( - children: [ - TextFormField( - key: fieldKey, - ), - TextFormField( - validator: errorText, - ), - ], + return MaterialApp( + home: MediaQuery( + data: const MediaQueryData(devicePixelRatio: 1.0), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: Form( + key: formKey, + autovalidate: true, + child: ListView( + children: [ + TextFormField( + key: fieldKey, + ), + TextFormField( + validator: errorText, + ), + ], + ), ), ), ), @@ -184,16 +192,18 @@ void main() { final GlobalKey> inputKey = GlobalKey>(); Widget builder() { - return MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: Material( - child: Form( - child: TextFormField( - key: inputKey, - initialValue: 'hello', + return MaterialApp( + home: MediaQuery( + data: const MediaQueryData(devicePixelRatio: 1.0), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: Form( + child: TextFormField( + key: inputKey, + initialValue: 'hello', + ), ), ), ), @@ -227,16 +237,18 @@ void main() { final GlobalKey> inputKey = GlobalKey>(); Widget builder() { - return MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: Material( - child: Form( - child: TextFormField( - key: inputKey, - controller: controller, + return MaterialApp( + home: MediaQuery( + data: const MediaQueryData(devicePixelRatio: 1.0), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: Form( + child: TextFormField( + key: inputKey, + controller: controller, + ), ), ), ), @@ -272,18 +284,20 @@ void main() { final TextEditingController controller = TextEditingController(text: 'Plover'); Widget builder() { - return MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: Material( - child: Form( - key: formKey, - child: TextFormField( - key: inputKey, - controller: controller, - // initialValue is 'Plover' + return MaterialApp( + home: MediaQuery( + data: const MediaQueryData(devicePixelRatio: 1.0), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: Form( + key: formKey, + child: TextFormField( + key: inputKey, + controller: controller, + // initialValue is 'Plover' + ), ), ), ), @@ -322,16 +336,18 @@ void main() { return StatefulBuilder( builder: (BuildContext context, StateSetter setter) { setState = setter; - return MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: Material( - child: Form( - child: TextFormField( - key: inputKey, - controller: currentController, + return MaterialApp( + home: MediaQuery( + data: const MediaQueryData(devicePixelRatio: 1.0), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: Form( + child: TextFormField( + key: inputKey, + controller: currentController, + ), ), ), ), @@ -420,18 +436,20 @@ void main() { String fieldValue; Widget builder(bool remove) { - return MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: Material( - child: Form( - key: formKey, - child: remove ? Container() : TextFormField( - autofocus: true, - onSaved: (String value) { fieldValue = value; }, - validator: (String value) { return value.isEmpty ? null : 'yes'; }, + return MaterialApp( + home: MediaQuery( + data: const MediaQueryData(devicePixelRatio: 1.0), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: Form( + key: formKey, + child: remove ? Container() : TextFormField( + autofocus: true, + onSaved: (String value) { fieldValue = value; }, + validator: (String value) { return value.isEmpty ? null : 'yes'; }, + ), ), ), ), diff --git a/packages/flutter/test/widgets/invert_colors_test.dart b/packages/flutter/test/widgets/invert_colors_test.dart index bedb76c3ed4..8489f6e09f0 100644 --- a/packages/flutter/test/widgets/invert_colors_test.dart +++ b/packages/flutter/test/widgets/invert_colors_test.dart @@ -24,7 +24,6 @@ void main() { 'invert_colors_test.0.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -46,7 +45,6 @@ void main() { 'invert_colors_test.1.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); } diff --git a/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart b/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart index eacb3e55936..b52e5388b87 100644 --- a/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart +++ b/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart @@ -539,7 +539,6 @@ void main() { 'list_wheel_scroll_view.center_child.magnified.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -597,7 +596,6 @@ void main() { 'list_wheel_scroll_view.curved_wheel.left.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); diff --git a/packages/flutter/test/widgets/opacity_test.dart b/packages/flutter/test/widgets/opacity_test.dart index 0446459ddd2..1d173b3c42b 100644 --- a/packages/flutter/test/widgets/opacity_test.dart +++ b/packages/flutter/test/widgets/opacity_test.dart @@ -181,7 +181,6 @@ void main() { 'opacity_test.offset.png', version: 1, ), - skip: !isLinux, ); }, skip: isBrowser); diff --git a/packages/flutter/test/widgets/physical_model_test.dart b/packages/flutter/test/widgets/physical_model_test.dart index c425fb41867..5e043f8ea36 100644 --- a/packages/flutter/test/widgets/physical_model_test.dart +++ b/packages/flutter/test/widgets/physical_model_test.dart @@ -114,7 +114,6 @@ void main() { 'physical_model_overflow.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); diff --git a/packages/flutter/test/widgets/shadow_test.dart b/packages/flutter/test/widgets/shadow_test.dart index bf5d6e2b269..f573974af17 100644 --- a/packages/flutter/test/widgets/shadow_test.dart +++ b/packages/flutter/test/widgets/shadow_test.dart @@ -37,8 +37,7 @@ void main() { 'shadow.BoxDecoration.enabled.png', version: null, ), - skip: !isLinux - ); // shadows render differently on different platforms + ); debugDisableShadows = true; }, skip: isBrowser); @@ -70,7 +69,7 @@ void main() { ); } debugDisableShadows = true; - }, skip: !isLinux); // shadows render differently on different platforms + }); testWidgets('Shadows with PhysicalLayer', (WidgetTester tester) async { await tester.pumpWidget( @@ -107,8 +106,7 @@ void main() { 'shadow.PhysicalModel.enabled.png', version: null, ), - skip: !isLinux, - ); // shadows render differently on different platforms + ); debugDisableShadows = true; }, skip: isBrowser); @@ -144,5 +142,5 @@ void main() { ); } debugDisableShadows = true; - }, skip: !isLinux); // shadows render differently on different platforms + }); } diff --git a/packages/flutter/test/widgets/table_test.dart b/packages/flutter/test/widgets/table_test.dart index e96c1ada904..3614d1e22b3 100644 --- a/packages/flutter/test/widgets/table_test.dart +++ b/packages/flutter/test/widgets/table_test.dart @@ -19,6 +19,24 @@ class TestStatefulWidgetState extends State { Widget build(BuildContext context) => Container(); } +class TestChildWidget extends StatefulWidget { + const TestChildWidget({ Key key }) : super(key: key); + + @override + TestChildState createState() => TestChildState(); +} + +class TestChildState extends State { + bool toggle = true; + + void toggleMe() { + setState(() { toggle = !toggle; }); + } + + @override + Widget build(BuildContext context) => toggle ? const SizedBox() : const Text('CRASHHH'); +} + void main() { testWidgets('Table widget - empty', (WidgetTester tester) async { await tester.pumpWidget( @@ -855,5 +873,32 @@ void main() { ); }); + // Regression test for https://github.com/flutter/flutter/issues/31473. + testWidgets( + 'Does not crash if a child RenderObject is replaced by another RenderObject of a different type', + (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Table(children: const [TableRow(children: [TestChildWidget()])]), + ), + ); + expect(find.text('CRASHHH'), findsNothing); + + final TestChildState state = tester.state(find.byType(TestChildWidget)); + state.toggleMe(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Table(children: const [TableRow(children: [TestChildWidget()])]), + ), + ); + + // Should not crash. + expect(find.text('CRASHHH'), findsOneWidget); + } + ); + // TODO(ianh): Test handling of TableCell object } diff --git a/packages/flutter/test/widgets/text_golden_test.dart b/packages/flutter/test/widgets/text_golden_test.dart index ce798a0efa1..4b0d29a8d12 100644 --- a/packages/flutter/test/widgets/text_golden_test.dart +++ b/packages/flutter/test/widgets/text_golden_test.dart @@ -62,7 +62,7 @@ void main() { version: null, ), ); - }, skip: !isLinux); + }); testWidgets('Text Foreground', (WidgetTester tester) async { @@ -147,7 +147,7 @@ void main() { version: null, ), ); - }, skip: !isLinux); + }); // TODO(garyq): This test requires an update when the background // drawing from the beginning of the line bug is fixed. The current @@ -200,7 +200,7 @@ void main() { version: null, ), ); - }, skip: !isLinux); + }); testWidgets('Text Fade', (WidgetTester tester) async { await tester.pumpWidget( @@ -239,7 +239,7 @@ void main() { version: 1, ), ); - }, skip: !isLinux); + }); testWidgets('Default Strut text', (WidgetTester tester) async { await tester.pumpWidget( @@ -267,8 +267,7 @@ void main() { version: null, ), ); - }, skip: true); // Should only be on linux (skip: !isLinux). - // Disabled for now until font inconsistency is resolved. + }); testWidgets('Strut text 1', (WidgetTester tester) async { await tester.pumpWidget( @@ -298,8 +297,7 @@ void main() { version: 1, ), ); - }, skip: true); // Should only be on linux (skip: !isLinux). - // Disabled for now until font inconsistency is resolved. + }); testWidgets('Strut text 2', (WidgetTester tester) async { await tester.pumpWidget( @@ -330,8 +328,7 @@ void main() { version: 1, ), ); - }, skip: true); // Should only be on linux (skip: !isLinux). - // Disabled for now until font inconsistency is resolved. + }); testWidgets('Strut text rich', (WidgetTester tester) async { await tester.pumpWidget( @@ -385,8 +382,7 @@ void main() { version: 1, ), ); - }, skip: true); // Should only be on linux (skip: !isLinux). - // Disabled for now until font inconsistency is resolved. + }); testWidgets('Strut text font fallback', (WidgetTester tester) async { // Font Fallback @@ -424,8 +420,7 @@ void main() { version: 1, ), ); - }, skip: true); // Should only be on linux (skip: !isLinux). - // Disabled for now until font inconsistency is resolved. + }); testWidgets('Strut text rich forceStrutHeight', (WidgetTester tester) async { await tester.pumpWidget( @@ -479,8 +474,7 @@ void main() { version: 1, ), ); - }, skip: true); // Should only be on linux (skip: !isLinux). - // Disabled for now until font inconsistency is resolved. + }); testWidgets('Decoration thickness', (WidgetTester tester) async { final TextDecoration allDecorations = TextDecoration.combine( @@ -521,7 +515,7 @@ void main() { version: 0, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Decoration thickness', (WidgetTester tester) async { final TextDecoration allDecorations = TextDecoration.combine( @@ -563,7 +557,7 @@ void main() { version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget', (WidgetTester tester) async { await tester.pumpWidget( @@ -660,7 +654,7 @@ void main() { version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget textfield', (WidgetTester tester) async { await tester.pumpWidget( @@ -708,7 +702,7 @@ void main() { version: 2, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); // This tests if multiple Text.rich widgets are able to inline nest within each other. testWidgets('Text Inline widget nesting', (WidgetTester tester) async { @@ -840,7 +834,7 @@ void main() { version: 2, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget baseline', (WidgetTester tester) async { await tester.pumpWidget( @@ -950,7 +944,7 @@ void main() { version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget aboveBaseline', (WidgetTester tester) async { await tester.pumpWidget( @@ -1060,7 +1054,7 @@ void main() { version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget belowBaseline', (WidgetTester tester) async { await tester.pumpWidget( @@ -1170,7 +1164,7 @@ void main() { version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget top', (WidgetTester tester) async { await tester.pumpWidget( @@ -1280,7 +1274,7 @@ void main() { version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); testWidgets('Text Inline widget middle', (WidgetTester tester) async { await tester.pumpWidget( @@ -1390,5 +1384,5 @@ void main() { version: 1, ), ); - }, skip: !isLinux); // Coretext uses different thicknesses for decoration + }); } diff --git a/packages/flutter/test/widgets/text_selection_test.dart b/packages/flutter/test/widgets/text_selection_test.dart index b978ef53988..41ebfe6c47f 100644 --- a/packages/flutter/test/widgets/text_selection_test.dart +++ b/packages/flutter/test/widgets/text_selection_test.dart @@ -5,6 +5,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/gestures.dart' show PointerDeviceKind; import 'package:flutter/widgets.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/material.dart'; void main() { int tapCount; @@ -62,6 +64,30 @@ void main() { ); } + Future pumpTextSelectionGestureDetectorBuilder( + WidgetTester tester, { + bool forcePressEnabled = true, + bool selectionEnabled = true, + }) async { + final GlobalKey editableTextKey = GlobalKey(); + final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( + editableTextKey: editableTextKey, + forcePressEnabled: forcePressEnabled, + selectionEnabled: selectionEnabled, + ); + final TextSelectionGestureDetectorBuilder provider = + TextSelectionGestureDetectorBuilder(delegate: delegate); + + await tester.pumpWidget( + MaterialApp( + home: provider.buildGestureDetector( + behavior: HitTestBehavior.translucent, + child: FakeEditableText(key: editableTextKey) + ) + ) + ); + } + testWidgets('a series of taps all call onTaps', (WidgetTester tester) async { await pumpGestureDetector(tester); await tester.tapAt(const Offset(200, 200)); @@ -380,4 +406,221 @@ void main() { await gesture.removePointer(); }); + + testWidgets('test TextSelectionGestureDetectorBuilder long press', (WidgetTester tester) async { + await pumpTextSelectionGestureDetectorBuilder(tester); + final TestGesture gesture = + await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch); + await tester.pump(const Duration(seconds: 2)); + await gesture.up(); + await tester.pumpAndSettle(); + + final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); + final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); + expect(state.showToolbarCalled, isTrue); + expect(renderEditable.selectPositionAtCalled, isTrue); + }); + + testWidgets('test TextSelectionGestureDetectorBuilder tap', (WidgetTester tester) async { + await pumpTextSelectionGestureDetectorBuilder(tester); + final TestGesture gesture = + await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch); + await gesture.up(); + await tester.pumpAndSettle(); + + final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); + final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); + expect(state.showToolbarCalled, isFalse); + expect(renderEditable.selectWordEdgeCalled, isTrue); + }); + + testWidgets('test TextSelectionGestureDetectorBuilder double tap', (WidgetTester tester) async { + await pumpTextSelectionGestureDetectorBuilder(tester); + final TestGesture gesture = + await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch); + await tester.pump(const Duration(milliseconds: 50)); + await gesture.up(); + await gesture.down(const Offset(200.0, 200.0)); + await tester.pump(const Duration(milliseconds: 50)); + await gesture.up(); + await tester.pumpAndSettle(); + + final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); + final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); + expect(state.showToolbarCalled, isTrue); + expect(renderEditable.selectWordCalled, isTrue); + }); + + testWidgets('test TextSelectionGestureDetectorBuilder forcePress enabled', (WidgetTester tester) async { + await pumpTextSelectionGestureDetectorBuilder(tester); + final TestGesture gesture = await tester.createGesture(); + await gesture.downWithCustomEvent( + const Offset(200.0, 200.0), + const PointerDownEvent( + pointer: 0, + position: Offset(200.0, 200.0), + pressure: 3.0, + pressureMax: 6.0, + pressureMin: 0.0, + ), + ); + await gesture.updateWithCustomEvent( + const PointerUpEvent( + pointer: 0, + position: Offset(200.0, 200.0), + pressure: 0.0, + pressureMax: 6.0, + pressureMin: 0.0, + ), + ); + await tester.pump(); + + final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); + final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); + expect(state.showToolbarCalled, isTrue); + expect(renderEditable.selectWordsInRangeCalled, isTrue); + }); + + testWidgets('test TextSelectionGestureDetectorBuilder selection disabled', (WidgetTester tester) async { + await pumpTextSelectionGestureDetectorBuilder(tester, selectionEnabled: false); + final TestGesture gesture = + await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch); + await tester.pump(const Duration(seconds: 2)); + await gesture.up(); + await tester.pumpAndSettle(); + + final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); + final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); + expect(state.showToolbarCalled, isTrue); + expect(renderEditable.selectWordsInRangeCalled, isFalse); + }); + + testWidgets('test TextSelectionGestureDetectorBuilder forcePress disabled', (WidgetTester tester) async { + await pumpTextSelectionGestureDetectorBuilder(tester, forcePressEnabled: false); + final TestGesture gesture = await tester.createGesture(); + await gesture.downWithCustomEvent( + const Offset(200.0, 200.0), + const PointerDownEvent( + pointer: 0, + position: Offset(200.0, 200.0), + pressure: 3.0, + pressureMax: 6.0, + pressureMin: 0.0, + ), + ); + await gesture.up(); + await tester.pump(); + + final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); + final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); + expect(state.showToolbarCalled, isFalse); + expect(renderEditable.selectWordsInRangeCalled, isFalse); + }); +} + +class FakeTextSelectionGestureDetectorBuilderDelegate implements TextSelectionGestureDetectorBuilderDelegate { + FakeTextSelectionGestureDetectorBuilderDelegate({ + this.editableTextKey, + this.forcePressEnabled, + this.selectionEnabled, + }); + + @override + final GlobalKey editableTextKey; + + @override + final bool forcePressEnabled; + + @override + final bool selectionEnabled; +} + +class FakeEditableText extends EditableText { + FakeEditableText({Key key}): super( + key: key, + controller: TextEditingController(), + focusNode: FocusNode(), + backgroundCursorColor: Colors.white, + cursorColor: Colors.white, + style: const TextStyle(), + ); + + @override + FakeEditableTextState createState() => FakeEditableTextState(); +} + +class FakeEditableTextState extends EditableTextState { + final GlobalKey _editableKey = GlobalKey(); + bool showToolbarCalled = false; + + @override + RenderEditable get renderEditable => _editableKey.currentContext.findRenderObject(); + + @override + bool showToolbar() { + showToolbarCalled = true; + return true; + } + + @override + Widget build(BuildContext context) { + super.build(context); + return FakeEditable(this, key: _editableKey); + } +} + +class FakeEditable extends LeafRenderObjectWidget { + const FakeEditable( + this.delegate, { + Key key, + }) : super(key: key); + final EditableTextState delegate; + + @override + RenderEditable createRenderObject(BuildContext context) { + return FakeRenderEditable(delegate); + } +} + +class FakeRenderEditable extends RenderEditable { + FakeRenderEditable(EditableTextState delegate) : super( + text: const TextSpan( + style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'), + text: 'placeholder', + ), + startHandleLayerLink: LayerLink(), + endHandleLayerLink: LayerLink(), + textAlign: TextAlign.start, + textDirection: TextDirection.ltr, + locale: const Locale('en', 'US'), + offset: ViewportOffset.fixed(10.0), + textSelectionDelegate: delegate, + selection: const TextSelection.collapsed( + offset: 0, + ), + ); + + bool selectWordsInRangeCalled = false; + @override + void selectWordsInRange({ @required Offset from, Offset to, @required SelectionChangedCause cause }) { + selectWordsInRangeCalled = true; + } + + bool selectWordEdgeCalled = false; + @override + void selectWordEdge({ @required SelectionChangedCause cause }) { + selectWordEdgeCalled = true; + } + + bool selectPositionAtCalled = false; + @override + void selectPositionAt({ @required Offset from, Offset to, @required SelectionChangedCause cause }) { + selectPositionAtCalled = true; + } + + bool selectWordCalled = false; + @override + void selectWord({ @required SelectionChangedCause cause }) { + selectWordCalled = true; + } } diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index c77591d5c23..09ea5f4f687 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -2028,7 +2028,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.repaint_boundary_margin.png', version: null, ), - skip: !isLinux, ); // Regression test for how rendering with a pixel scale other than 1.0 @@ -2042,7 +2041,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.repaint_boundary_margin_small.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2054,7 +2052,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.repaint_boundary_margin_large.png', version: null, ), - skip: !isLinux, ); final Layer layerParent = layer.parent; @@ -2073,7 +2070,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.repaint_boundary.png', version: null, ), - skip: !isLinux, ); // Verify that taking a screenshot didn't change the layers associated with @@ -2094,7 +2090,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.repaint_boundary_margin.png', version: null, ), - skip: !isLinux, ); // Verify that taking a screenshot didn't change the layers associated with @@ -2118,7 +2113,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.repaint_boundary_debugPaint.png', version: null, ), - skip: !isLinux, ); // Verify that taking a screenshot with debug paint on did not change // the number of children the layer has. @@ -2132,7 +2126,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.repaint_boundary.png', version: null, ), - skip: !isLinux, ); expect(renderObject.debugLayer, equals(layer)); @@ -2149,7 +2142,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.container.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2163,7 +2155,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.container_debugPaint.png', version: null, ), - skip: !isLinux, ); { @@ -2187,7 +2178,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.container_debugPaint.png', version: null, ), - skip: !isLinux, ); expect(container.debugNeedsLayout, isFalse); } @@ -2203,7 +2193,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.container_small.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2217,7 +2206,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.container_large.png', version: null, ), - skip: !isLinux, ); // This screenshot will show the clip rect debug paint but no other @@ -2233,7 +2221,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.clipRect_debugPaint.png', version: null, ), - skip: !isLinux, ); final Element clipRect = find.byType(ClipRRect).evaluate().single; @@ -2253,7 +2240,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.clipRect_debugPaint_margin.png', version: null, ), - skip: !isLinux, ); // Verify we get the same image if we go through the service extension @@ -2296,7 +2282,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.padding_debugPaint.png', version: null, ), - skip: !isLinux, ); // The bounds for this box crop its rendered content. @@ -2311,7 +2296,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.sizedBox_debugPaint.png', version: 1, ), - skip: !isLinux, ); // Verify that setting a margin includes the previously cropped content. @@ -2327,7 +2311,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.sizedBox_debugPaint_margin.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -2402,7 +2385,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.composited_transform.only_offsets.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2415,7 +2397,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.composited_transform.only_offsets_follower.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2424,7 +2405,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.composited_transform.only_offsets_small.png', version: 1, ), - skip: !isLinux, ); await expectLater( @@ -2437,7 +2417,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.composited_transform.only_offsets_target.png', version: null, ), - skip: !isLinux, ); }, skip: isBrowser); @@ -2513,7 +2492,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.composited_transform.with_rotations.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2526,7 +2504,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.composited_transform.with_rotations_small.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2539,7 +2516,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.composited_transform.with_rotations_target.png', version: null, ), - skip: !isLinux, ); await expectLater( @@ -2552,7 +2528,6 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { 'inspector.composited_transform.with_rotations_follower.png', version: null, ), - skip: !isLinux, ); // Make sure taking screenshots hasn't modified the positions of the diff --git a/packages/flutter_driver/pubspec.yaml b/packages/flutter_driver/pubspec.yaml index 15993722dd3..b58f3f622a2 100644 --- a/packages/flutter_driver/pubspec.yaml +++ b/packages/flutter_driver/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: json_rpc_2: 2.1.0 meta: 1.1.6 path: 1.6.2 - web_socket_channel: 1.0.13 + web_socket_channel: 1.0.14 vm_service_client: 0.2.6+2 flutter: sdk: flutter @@ -46,4 +46,4 @@ dev_dependencies: mockito: 4.1.0 quiver: 2.0.3 -# PUBSPEC CHECKSUM: fc49 +# PUBSPEC CHECKSUM: fd4a diff --git a/packages/flutter_goldens/lib/flutter_goldens.dart b/packages/flutter_goldens/lib/flutter_goldens.dart index 4f28df3576e..3784c0d1b90 100644 --- a/packages/flutter_goldens/lib/flutter_goldens.dart +++ b/packages/flutter_goldens/lib/flutter_goldens.dart @@ -9,19 +9,88 @@ import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:meta/meta.dart'; +import 'package:platform/platform.dart'; import 'package:flutter_goldens_client/client.dart'; +import 'package:flutter_goldens_client/skia_client.dart'; + export 'package:flutter_goldens_client/client.dart'; +export 'package:flutter_goldens_client/skia_client.dart'; /// Main method that can be used in a `flutter_test_config.dart` file to set /// [goldenFileComparator] to an instance of [FlutterGoldenFileComparator] that -/// works for the current test. +/// works for the current test. _Which_ FlutterGoldenFileComparator is +/// instantiated is based on the current testing environment. Future main(FutureOr testMain()) async { - goldenFileComparator = await FlutterGoldenFileComparator.fromDefaultComparator(); + const Platform platform = LocalPlatform(); + if (FlutterSkiaGoldFileComparator.isAvailableOnPlatform(platform)) { + goldenFileComparator = await FlutterSkiaGoldFileComparator.fromDefaultComparator(); + } else if (FlutterGoldensRepositoryFileComparator.isAvailableOnPlatform(platform)) { + goldenFileComparator = await FlutterGoldensRepositoryFileComparator.fromDefaultComparator(); + } else { + goldenFileComparator = FlutterSkippingGoldenFileComparator.fromDefaultComparator(); + } await testMain(); } -/// A golden file comparator specific to the `flutter/flutter` repository. +/// Abstract base class golden file comparator specific to the `flutter/flutter` +/// repository. +abstract class FlutterGoldenFileComparator extends GoldenFileComparator { + /// Creates a [FlutterGoldenFileComparator] that will resolve golden file + /// URIs relative to the specified [basedir]. + /// + /// The [fs] and [platform] parameters useful in tests, where the default file + /// system and platform can be replaced by mock instances. + @visibleForTesting + FlutterGoldenFileComparator( + this.basedir, { + this.fs = const LocalFileSystem(), + this.platform = const LocalPlatform(), + }) : assert(basedir != null), + assert(fs != null), + assert(platform != null); + + /// The directory to which golden file URIs will be resolved in [compare] and + /// [update]. + final Uri basedir; + + /// The file system used to perform file access. + @visibleForTesting + final FileSystem fs; + + /// A wrapper for the [dart:io.Platform] API. + @visibleForTesting + final Platform platform; + + @override + Future update(Uri golden, Uint8List imageBytes) async { + final File goldenFile = getGoldenFile(golden); + await goldenFile.parent.create(recursive: true); + await goldenFile.writeAsBytes(imageBytes, flush: true); + } + + /// Calculate the appropriate basedir for the current test context. + @protected + @visibleForTesting + static Directory getBaseDirectory(GoldensClient goldens, LocalFileComparator defaultComparator) { + final FileSystem fs = goldens.fs; + final Directory testDirectory = fs.directory(defaultComparator.basedir); + final String testDirectoryRelativePath = fs.path.relative(testDirectory.path, from: goldens.flutterRoot.path); + return goldens.comparisonRoot.childDirectory(testDirectoryRelativePath); + } + + /// Returns the golden [File] identified by the given [Uri]. + @protected + File getGoldenFile(Uri uri) { + assert(basedir.scheme == 'file'); + final File goldenFile = fs.directory(basedir).childFile(fs.file(uri).path); + assert(goldenFile.uri.scheme == 'file'); + return goldenFile; + } +} + +/// A [FlutterGoldenFileComparator] for testing golden images against the +/// `flutter/goldens` repository. /// /// Within the https://github.com/flutter/flutter repository, it's important /// not to check-in binaries in order to keep the size of the repository to a @@ -29,59 +98,61 @@ Future main(FutureOr testMain()) async { /// files from a sibling repository, `flutter/goldens`. /// /// This comparator will locally clone the `flutter/goldens` repository into -/// the `$FLUTTER_ROOT/bin/cache/pkg/goldens` folder, then perform the comparison against -/// the files therein. -class FlutterGoldenFileComparator implements GoldenFileComparator { - /// Creates a [FlutterGoldenFileComparator] that will resolve golden file - /// URIs relative to the specified [basedir]. +/// the `$FLUTTER_ROOT/bin/cache/pkg/goldens` folder using the +/// [GoldensRepositoryClient], then perform the comparison against the files +/// therein. +/// +/// See also: +/// +/// * [GoldenFileComparator], the abstract class that +/// [FlutterGoldenFileComparator] implements. +/// * [FlutterSkiaGoldFileComparator], another [FlutterGoldenFileComparator] +/// that tests golden images through Skia Gold. +class FlutterGoldensRepositoryFileComparator extends FlutterGoldenFileComparator { + /// Creates a [FlutterGoldensRepositoryFileComparator] that will test golden + /// file images against the `flutter/goldens` repository. /// - /// The [fs] parameter exists for testing purposes only. - @visibleForTesting - FlutterGoldenFileComparator( - this.basedir, { - this.fs = const LocalFileSystem(), - }); + /// The [fs] and [platform] parameters useful in tests, where the default file + /// system and platform can be replaced by mock instances. + FlutterGoldensRepositoryFileComparator( + Uri basedir, { + FileSystem fs = const LocalFileSystem(), + Platform platform = const LocalPlatform(), + }) : super( + basedir, + fs: fs, + platform: platform, + ); - /// The directory to which golden file URIs will be resolved in [compare] and [update]. - final Uri basedir; - - /// The file system used to perform file access. - @visibleForTesting - final FileSystem fs; - - /// Creates a new [FlutterGoldenFileComparator] that mirrors the relative - /// path resolution of the default [goldenFileComparator]. + /// Creates a new [FlutterGoldensRespositoryFileComparator] that mirrors the + /// relative path resolution of the default [goldenFileComparator]. /// /// By the time the future completes, the clone of the `flutter/goldens` - /// repository is guaranteed to be ready use. + /// repository is guaranteed to be ready to use. /// /// The [goldens] and [defaultComparator] parameters are visible for testing /// purposes only. - static Future fromDefaultComparator({ - GoldensClient goldens, + static Future fromDefaultComparator({ + GoldensRepositoryClient goldens, LocalFileComparator defaultComparator, }) async { defaultComparator ??= goldenFileComparator; // Prepare the goldens repo. - goldens ??= GoldensClient(); + goldens ??= GoldensRepositoryClient(); await goldens.prepare(); - // Calculate the appropriate basedir for the current test context. - final FileSystem fs = goldens.fs; - final Directory testDirectory = fs.directory(defaultComparator.basedir); - final String testDirectoryRelativePath = fs.path.relative(testDirectory.path, from: goldens.flutterRoot.path); - return FlutterGoldenFileComparator(goldens.repositoryRoot.childDirectory(testDirectoryRelativePath).uri); + final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(goldens, defaultComparator); + return FlutterGoldensRepositoryFileComparator(baseDirectory.uri); } @override Future compare(Uint8List imageBytes, Uri golden) async { - final File goldenFile = _getGoldenFile(golden); + final File goldenFile = getGoldenFile(golden); if (!goldenFile.existsSync()) { throw TestFailure('Could not be compared against non-existent file: "$golden"'); } final List goldenBytes = await goldenFile.readAsBytes(); - // TODO(tvolkert): Improve the intelligence of this comparison. if (goldenBytes.length != imageBytes.length) { return false; } @@ -93,14 +164,130 @@ class FlutterGoldenFileComparator implements GoldenFileComparator { return true; } - @override - Future update(Uri golden, Uint8List imageBytes) async { - final File goldenFile = _getGoldenFile(golden); - await goldenFile.parent.create(recursive: true); - await goldenFile.writeAsBytes(imageBytes, flush: true); + /// Decides based on the current platform whether goldens tests should be + /// performed against the flutter/goldens repository. + static bool isAvailableOnPlatform(Platform platform) => platform.isLinux; +} + +/// A [FlutterGoldenFileComparator] for testing golden images with Skia Gold. +/// +/// For testing across all platforms, the [SkiaGoldClient] is used to upload +/// images for framework-related golden tests and process results. Currently +/// these tests are designed to be run post-submit on Cirrus CI, informed by the +/// environment. +/// +/// See also: +/// +/// * [GoldenFileComparator], the abstract class that +/// [FlutterGoldenFileComparator] implements. +/// * [FlutterGoldensRepositoryFileComparator], another +/// [FlutterGoldenFileComparator] that tests golden images using the +/// flutter/goldens repository. +class FlutterSkiaGoldFileComparator extends FlutterGoldenFileComparator { + /// Creates a [FlutterSkiaGoldFileComparator] that will test golden file + /// images against Skia Gold. + /// + /// The [fs] and [platform] parameters useful in tests, where the default file + /// system and platform can be replaced by mock instances. + FlutterSkiaGoldFileComparator( + final Uri basedir, + this.skiaClient, { + FileSystem fs = const LocalFileSystem(), + Platform platform = const LocalPlatform(), + }) : super( + basedir, + fs: fs, + platform: platform, + ); + + final SkiaGoldClient skiaClient; + + /// Creates a new [FlutterSkiaGoldFileComparator] that mirrors the relative + /// path resolution of the default [goldenFileComparator]. + /// + /// The [goldens] and [defaultComparator] parameters are visible for testing + /// purposes only. + static Future fromDefaultComparator({ + SkiaGoldClient goldens, + LocalFileComparator defaultComparator, + }) async { + defaultComparator ??= goldenFileComparator; + goldens ??= SkiaGoldClient(); + + final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(goldens, defaultComparator); + if (!baseDirectory.existsSync()) + baseDirectory.createSync(recursive: true); + await goldens.auth(baseDirectory); + await goldens.imgtestInit(); + return FlutterSkiaGoldFileComparator(baseDirectory.uri, goldens); } - File _getGoldenFile(Uri uri) { - return fs.directory(basedir).childFile(fs.file(uri).path); + @override + Future compare(Uint8List imageBytes, Uri golden) async { + golden = _addPrefix(golden); + await update(golden, imageBytes); + + final File goldenFile = getGoldenFile(golden); + if (!goldenFile.existsSync()) { + throw TestFailure('Could not be compared against non-existent file: "$golden"'); + } + return await skiaClient.imgtestAdd(golden.path, goldenFile); + } + + @override + Uri getTestUri(Uri key, int version) => key; + + /// Decides based on the current environment whether goldens tests should be + /// performed against Skia Gold. + static bool isAvailableOnPlatform(Platform platform) { + final String cirrusCI = platform.environment['CIRRUS_CI'] ?? ''; + final String cirrusPR = platform.environment['CIRRUS_PR'] ?? ''; + final String cirrusBranch = platform.environment['CIRRUS_BRANCH'] ?? ''; + return cirrusCI.isNotEmpty && cirrusPR.isEmpty && cirrusBranch == 'master'; + } + + /// Prepends the golden Uri with the library name that encloses the current + /// test. + Uri _addPrefix(Uri golden) { + final String prefix = basedir.pathSegments[basedir.pathSegments.length - 2]; + return Uri.parse(prefix + '.' + golden.toString()); } } + +/// A [FlutterGoldenFileComparator] for skipping golden image tests when Skia +/// Gold is unavailable or the current platform that is executing tests is not +/// Linux. +/// +/// See also: +/// +/// * [FlutterGoldensRepositoryFileComparator], another +/// [FlutterGoldenFileComparator] that tests golden images using the +/// flutter/goldens repository. +/// * [FlutterSkiaGoldFileComparator], another [FlutterGoldenFileComparator] +/// that tests golden images through Skia Gold. +class FlutterSkippingGoldenFileComparator extends FlutterGoldenFileComparator { + /// Creates a [FlutterSkippingGoldenFileComparator] that will skip tests that + /// are not in the right environment for golden file testing. + FlutterSkippingGoldenFileComparator(Uri basedir) : super(basedir); + + /// Creates a new [FlutterSkippingGoldenFileComparator] that mirrors the relative + /// path resolution of the default [goldenFileComparator]. + static FlutterSkippingGoldenFileComparator fromDefaultComparator({ + LocalFileComparator defaultComparator, + }) { + defaultComparator ??= goldenFileComparator; + return FlutterSkippingGoldenFileComparator(defaultComparator.basedir); + } + + @override + Future compare(Uint8List imageBytes, Uri golden) async { + print('Skipping "$golden" test : Skia Gold is not available in this testing ' + 'environment and flutter/goldens repository comparison is only available ' + 'on Linux machines.' + ); + return true; + } + + @override + Future update(Uri golden, Uint8List imageBytes) => null; +} diff --git a/packages/flutter_goldens/test/flutter_goldens_test.dart b/packages/flutter_goldens/test/flutter_goldens_test.dart index 19645edc8f2..4f1520052b2 100644 --- a/packages/flutter_goldens/test/flutter_goldens_test.dart +++ b/packages/flutter_goldens/test/flutter_goldens_test.dart @@ -34,13 +34,13 @@ void main() { }); group('GoldensClient', () { - GoldensClient goldens; + GoldensRepositoryClient goldens; setUp(() { - goldens = GoldensClient( + goldens = GoldensRepositoryClient( fs: fs, - platform: platform, process: process, + platform: platform, ); }); @@ -60,32 +60,65 @@ void main() { }); }); + group('SkiaGoldClient', () { + SkiaGoldClient goldens; + + setUp(() { + goldens = SkiaGoldClient( + fs: fs, + process: process, + platform: platform, + ); + }); + + group('auth', () { + test('performs minimal work if already authorized', () async { + final Directory workDirectory = fs.directory('/workDirectory')..createSync(recursive: true); + fs.file('/workDirectory/temp/auth_opt.json')..createSync(recursive: true); + when(process.run(any)).thenAnswer((_) => Future.value(io.ProcessResult(123, 0, '', ''))); + await goldens.auth(workDirectory); + + // Verify that we spawned no process calls + final VerificationResult verifyProcessRun = + verifyNever(process.run(captureAny, workingDirectory: captureAnyNamed('workingDirectory'))); + expect(verifyProcessRun.callCount, 0); + }); + }); + }); + group('FlutterGoldenFileComparator', () { + test('calculates the basedir correctly', () async { + final MockSkiaGoldClient goldens = MockSkiaGoldClient(); + final MockLocalFileComparator defaultComparator = MockLocalFileComparator(); + final Directory flutterRoot = fs.directory('/foo')..createSync(recursive: true); + final Directory goldensRoot = flutterRoot.childDirectory('bar')..createSync(recursive: true); + when(goldens.fs).thenReturn(fs); + when(goldens.flutterRoot).thenReturn(flutterRoot); + when(goldens.comparisonRoot).thenReturn(goldensRoot); + when(defaultComparator.basedir).thenReturn(flutterRoot.childDirectory('baz').uri); + final Directory basedir = FlutterGoldenFileComparator.getBaseDirectory(goldens, defaultComparator); + expect(basedir.uri, fs.directory('/foo/bar/baz').uri); + }); + }); + + group('FlutterGoldensRepositoryFileComparator', () { MemoryFileSystem fs; - FlutterGoldenFileComparator comparator; + FlutterGoldensRepositoryFileComparator comparator; setUp(() { fs = MemoryFileSystem(); + platform = FakePlatform( + operatingSystem: 'linux', + environment: {'FLUTTER_ROOT': _kFlutterRoot}, + ); final Directory flutterRoot = fs.directory('/path/to/flutter')..createSync(recursive: true); final Directory goldensRoot = flutterRoot.childDirectory('bin/cache/goldens')..createSync(recursive: true); final Directory testDirectory = goldensRoot.childDirectory('test/foo/bar')..createSync(recursive: true); - comparator = FlutterGoldenFileComparator(testDirectory.uri, fs: fs); - }); - - group('fromDefaultComparator', () { - test('calculates the basedir correctly', () async { - final MockGoldensClient goldens = MockGoldensClient(); - final MockLocalFileComparator defaultComparator = MockLocalFileComparator(); - final Directory flutterRoot = fs.directory('/foo')..createSync(recursive: true); - final Directory goldensRoot = flutterRoot.childDirectory('bar')..createSync(recursive: true); - when(goldens.fs).thenReturn(fs); - when(goldens.flutterRoot).thenReturn(flutterRoot); - when(goldens.repositoryRoot).thenReturn(goldensRoot); - when(defaultComparator.basedir).thenReturn(flutterRoot.childDirectory('baz').uri); - comparator = await FlutterGoldenFileComparator.fromDefaultComparator( - goldens: goldens, defaultComparator: defaultComparator); - expect(comparator.basedir, fs.directory('/foo/bar/baz').uri); - }); + comparator = FlutterGoldensRepositoryFileComparator( + testDirectory.uri, + fs: fs, + platform: platform, + ); }); group('compare', () { @@ -132,9 +165,44 @@ void main() { expect(goldenFile.readAsBytesSync(), [1, 2, 3]); }); }); + + group('getTestUri', () { + test('incorporates version number', () { + final Uri key = comparator.getTestUri(Uri.parse('foo.png'), 1); + expect(key, Uri.parse('foo.1.png')); + }); + test('ignores null version number', () { + final Uri key = comparator.getTestUri(Uri.parse('foo.png'), null); + expect(key, Uri.parse('foo.png')); + }); + }); + }); + + group('FlutterSkiaGoldFileComparator', () { + FlutterSkiaGoldFileComparator comparator; + + setUp(() { + final Directory flutterRoot = fs.directory('/path/to/flutter')..createSync(recursive: true); + final Directory goldensRoot = flutterRoot.childDirectory('bin/cache/goldens')..createSync(recursive: true); + final Directory testDirectory = goldensRoot.childDirectory('test/foo/bar')..createSync(recursive: true); + comparator = FlutterSkiaGoldFileComparator( + testDirectory.uri, + MockSkiaGoldClient(), + fs: fs, + platform: platform, + ); + }); + + group('getTestUri', () { + test('ignores version number', () { + final Uri key = comparator.getTestUri(Uri.parse('foo.png'), 1); + expect(key, Uri.parse('foo.png')); + }); + }); }); } class MockProcessManager extends Mock implements ProcessManager {} -class MockGoldensClient extends Mock implements GoldensClient {} +class MockGoldensRepositoryClient extends Mock implements GoldensRepositoryClient {} +class MockSkiaGoldClient extends Mock implements SkiaGoldClient {} class MockLocalFileComparator extends Mock implements LocalFileComparator {} diff --git a/packages/flutter_goldens_client/lib/client.dart b/packages/flutter_goldens_client/lib/client.dart index 3f6952f3a5e..d5257a52d38 100644 --- a/packages/flutter_goldens_client/lib/client.dart +++ b/packages/flutter_goldens_client/lib/client.dart @@ -16,11 +16,11 @@ import 'package:process/process.dart'; const String _kFlutterRootKey = 'FLUTTER_ROOT'; -/// A class that represents a clone of the https://github.com/flutter/goldens -/// repository, nested within the `bin/cache` directory of the caller's Flutter -/// repository. -class GoldensClient { - /// Create a handle to a local clone of the goldens repository. +/// An base class that provides shared information to the +/// [FlutterGoldenFileComparator] as well as the [SkiaGoldClient] and +/// [GoldensRepositoryClient]. +abstract class GoldensClient { + /// Creates a handle to the local environment of golden file images. GoldensClient({ this.fs = const LocalFileSystem(), this.platform = const LocalPlatform(), @@ -46,17 +46,32 @@ class GoldensClient { /// subprocesses. final ProcessManager process; - RandomAccessFile _lock; - /// The local [Directory] where the Flutter repository is hosted. /// /// Uses the [fs] file system. Directory get flutterRoot => fs.directory(platform.environment[_kFlutterRootKey]); - /// The local [Directory] where the goldens repository is hosted. + /// The local [Directory] where the goldens files are located. /// /// Uses the [fs] file system. - Directory get repositoryRoot => flutterRoot.childDirectory(fs.path.join('bin', 'cache', 'pkg', 'goldens')); + Directory get comparisonRoot => flutterRoot.childDirectory(fs.path.join('bin', 'cache', 'pkg', 'goldens')); +} + +/// A class that represents a clone of the https://github.com/flutter/goldens +/// repository, nested within the `bin/cache` directory of the caller's Flutter +/// repository. +class GoldensRepositoryClient extends GoldensClient { + GoldensRepositoryClient({ + FileSystem fs = const LocalFileSystem(), + ProcessManager process = const LocalProcessManager(), + Platform platform = const LocalPlatform(), + }) : super( + fs: fs, + process: process, + platform: platform, + ); + + RandomAccessFile _lock; /// Prepares the local clone of the `flutter/goldens` repository for golden /// file testing. @@ -89,46 +104,46 @@ class GoldensClient { } } - Future _getGoldensCommit() async { - final File versionFile = flutterRoot.childFile(fs.path.join('bin', 'internal', 'goldens.version')); - return (await versionFile.readAsString()).trim(); - } - Future _getCurrentCommit() async { - if (!repositoryRoot.existsSync()) { + if (!comparisonRoot.existsSync()) { return null; } else { final io.ProcessResult revParse = await process.run( ['git', 'rev-parse', 'HEAD'], - workingDirectory: repositoryRoot.path, + workingDirectory: comparisonRoot.path, ); return revParse.exitCode == 0 ? revParse.stdout.trim() : null; } } + Future _getGoldensCommit() async { + final File versionFile = flutterRoot.childFile(fs.path.join('bin', 'internal', 'goldens.version')); + return (await versionFile.readAsString()).trim(); + } + Future _initRepository() async { - await repositoryRoot.create(recursive: true); + await comparisonRoot.create(recursive: true); await _runCommands( [ 'git init', 'git remote add upstream https://github.com/flutter/goldens.git', 'git remote set-url --push upstream git@github.com:flutter/goldens.git', ], - workingDirectory: repositoryRoot, + workingDirectory: comparisonRoot, ); } Future _checkCanSync() async { final io.ProcessResult result = await process.run( ['git', 'status', '--porcelain'], - workingDirectory: repositoryRoot.path, + workingDirectory: comparisonRoot.path, ); if (result.stdout.trim().isNotEmpty) { final StringBuffer buf = StringBuffer(); buf - ..writeln('flutter_goldens git checkout at ${repositoryRoot.path} has local changes and cannot be synced.') + ..writeln('flutter_goldens git checkout at ${comparisonRoot.path} has local changes and cannot be synced.') ..writeln('To reset your client to a clean state, and lose any local golden test changes:') - ..writeln('cd ${repositoryRoot.path}') + ..writeln('cd ${comparisonRoot.path}') ..writeln('git reset --hard HEAD') ..writeln('git clean -x -d -f -f'); throw NonZeroExitCode(1, buf.toString()); @@ -142,7 +157,7 @@ class GoldensClient { 'git fetch upstream $commit', 'git reset --hard FETCH_HEAD', ], - workingDirectory: repositoryRoot, + workingDirectory: comparisonRoot, ); } @@ -174,6 +189,7 @@ class GoldensClient { _lock = null; } } + /// Exception that signals a process' exit with a non-zero exit code. class NonZeroExitCode implements Exception { /// Create an exception that represents a non-zero exit code. diff --git a/packages/flutter_goldens_client/lib/skia_client.dart b/packages/flutter_goldens_client/lib/skia_client.dart new file mode 100644 index 00000000000..fe3d7be285d --- /dev/null +++ b/packages/flutter_goldens_client/lib/skia_client.dart @@ -0,0 +1,211 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:path/path.dart' as path; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; + +import 'package:flutter_goldens_client/client.dart'; + +// If you are here trying to figure out how to use golden files in the Flutter +// repo itself, consider reading this wiki page: +// https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package%3Aflutter + +// TODO(Piinks): This file will replace ./client.dart when transition to Skia +// Gold testing is complete + +const String _kGoldctlKey = 'GOLDCTL'; +const String _kServiceAccountKey = 'GOLD_SERVICE_ACCOUNT'; + +/// An extension of the [GoldensClient] class that interfaces with Skia Gold +/// for golden file testing. +class SkiaGoldClient extends GoldensClient { + SkiaGoldClient({ + FileSystem fs = const LocalFileSystem(), + ProcessManager process = const LocalProcessManager(), + Platform platform = const LocalPlatform(), + }) : super( + fs: fs, + process: process, + platform: platform, + ); + + /// The local [Directory] within the [comparisonRoot] for the current test + /// context. In this directory, the client will create image and json files + /// for the goldctl tool to use. + /// + /// This is informed by the [FlutterGoldenFileComparator] [basedir]. It cannot + /// be null. + Directory _workDirectory; + + /// The path to the local [Directory] where the goldctl tool is hosted. + /// + /// Uses the [platform] environment in this implementation. + String get _goldctl => platform.environment[_kGoldctlKey]; + + /// The path to the local [Directory] where the service account key is + /// hosted. + /// + /// Uses the [platform] environment in this implementation. + String get _serviceAccount => platform.environment[_kServiceAccountKey]; + + /// Prepares the local work space for golden file testing and calls the + /// goldctl `auth` command. + /// + /// This ensures that the goldctl tool is authorized and ready for testing. It + /// will only be called once for each instance of + /// [FlutterSkiaGoldFileComparator]. + /// + /// The [workDirectory] parameter specifies the current directory that golden + /// tests are executing in, relative to the library of the given test. It is + /// informed by the basedir of the [FlutterSkiaGoldFileComparator]. + Future auth(Directory workDirectory) async { + assert(workDirectory != null); + _workDirectory = workDirectory; + if (_clientIsAuthorized()) + return; + + final File authorization = _workDirectory.childFile('serviceAccount.json'); + await authorization.writeAsString(_serviceAccount); + + final List authArguments = [ + 'auth', + '--service-account', authorization.path, + '--work-dir', _workDirectory.childDirectory('temp').path, + ]; + + final io.ProcessResult authResults = await io.Process.run( + _goldctl, + authArguments, + ); + + if (authResults.exitCode != 0) { + final StringBuffer buf = StringBuffer() + ..writeln('Flutter + Skia Gold auth failed.') + ..writeln('stdout: ${authResults.stdout}') + ..writeln('stderr: ${authResults.stderr}'); + throw NonZeroExitCode(authResults.exitCode, buf.toString()); + } + } + + /// Executes the `imgtest init` command in the goldctl tool. + /// + /// The `imgtest` command collects and uploads test results to the Skia Gold + /// backend, the `init` argument initializes the testing environment. + Future imgtestInit() async { + final String commitHash = await _getCurrentCommit(); + final File keys = _workDirectory.childFile('keys.json'); + final File failures = _workDirectory.childFile('failures.json'); + + await keys.writeAsString(_getKeysJSON()); + await failures.create(); + + final List imgtestInitArguments = [ + 'imgtest', 'init', + '--instance', 'flutter', + '--work-dir', _workDirectory.childDirectory('temp').path, + '--commit', commitHash, + '--keys-file', keys.path, + '--failure-file', failures.path, + '--passfail', + ]; + + if (imgtestInitArguments.contains(null)) { + final StringBuffer buf = StringBuffer(); + buf.writeln('Null argument for Skia Gold imgtest init:'); + imgtestInitArguments.forEach(buf.writeln); + throw NonZeroExitCode(1, buf.toString()); + } + + final io.ProcessResult imgtestInitResult = await io.Process.run( + _goldctl, + imgtestInitArguments, + ); + + if (imgtestInitResult.exitCode != 0) { + final StringBuffer buf = StringBuffer() + ..writeln('Flutter + Skia Gold imgtest init failed.') + ..writeln('stdout: ${imgtestInitResult.stdout}') + ..writeln('stderr: ${imgtestInitResult.stderr}'); + throw NonZeroExitCode(imgtestInitResult.exitCode, buf.toString()); + } + } + + /// Executes the `imgtest add` command in the goldctl tool. + /// + /// The `imgtest` command collects and uploads test results to the Skia Gold + /// backend, the `add` argument uploads the current image test. A response is + /// returned from the invocation of this command that indicates a pass or fail + /// result. + /// + /// The testName and goldenFile parameters reference the current comparison + /// being evaluated by the [FlutterSkiaGoldFileComparator]. + Future imgtestAdd(String testName, File goldenFile) async { + assert(testName != null); + assert(goldenFile != null); + + final List imgtestArguments = [ + 'imgtest', 'add', + '--work-dir', _workDirectory.childDirectory('temp').path, + '--test-name', testName.split(path.extension(testName.toString()))[0], + '--png-file', goldenFile.path, + ]; + + await io.Process.run( + _goldctl, + imgtestArguments, + ); + + // TODO(Piinks): Comment on PR if triage is needed, https://github.com/flutter/flutter/issues/34673 + // So as not to turn the tree red in this initial implementation, this will + // return true for now. + // The ProcessResult that returns from line 157 contains the pass/fail + // result of the test & links to the dashboard and diffs. + return true; + } + + /// Returns the current commit hash of the Flutter repository. + Future _getCurrentCommit() async { + if (!flutterRoot.existsSync()) { + final StringBuffer buf = StringBuffer() + ..writeln('Flutter root could not be found: $flutterRoot'); + throw NonZeroExitCode(1, buf.toString()); + } else { + final io.ProcessResult revParse = await process.run( + ['git', 'rev-parse', 'HEAD'], + workingDirectory: flutterRoot.path, + ); + return revParse.exitCode == 0 ? revParse.stdout.trim() : null; + } + } + + /// Returns a JSON String with keys value pairs used to uniquely identify the + /// configuration that generated the given golden file. + /// + /// Currently, the only key value pair being tracked is the platform the image + /// was rendered on. + String _getKeysJSON() { + return json.encode( + { + 'Platform' : platform.operatingSystem, + } + ); + } + + /// Returns a boolean value to prevent the client from re-authorizing itself + /// for multiple tests. + bool _clientIsAuthorized() { + final File authFile = _workDirectory.childFile(super.fs.path.join( + 'temp', + 'auth_opt.json', + )); + return authFile.existsSync(); + } +} diff --git a/packages/flutter_test/lib/src/goldens.dart b/packages/flutter_test/lib/src/goldens.dart index 6672755ce08..1bc6fcd8398 100644 --- a/packages/flutter_test/lib/src/goldens.dart +++ b/packages/flutter_test/lib/src/goldens.dart @@ -45,6 +45,26 @@ abstract class GoldenFileComparator { /// The method by which [golden] is located and by which its bytes are written /// is left up to the implementation class. Future update(Uri golden, Uint8List imageBytes); + + /// Returns a new golden file [Uri] to incorporate any [version] number with + /// the [key]. + /// + /// The [version] is an optional int that can be used to differentiate + /// historical golden files. + /// + /// Version numbers are used in golden file tests for package:flutter. You can + /// learn more about these tests [here](https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter). + Uri getTestUri(Uri key, int version) { + if (version == null) + return key; + final String keyString = key.toString(); + final String extension = path.extension(keyString); + return Uri.parse( + keyString + .split(extension) + .join() + '.' + version.toString() + extension + ); + } } /// Compares rasterized image bytes against a golden image file. @@ -126,6 +146,11 @@ class TrivialComparator implements GoldenFileComparator { Future update(Uri golden, Uint8List imageBytes) { throw StateError('goldenFileComparator has not been initialized'); } + + @override + Uri getTestUri(Uri key, int version) { + return key; + } } /// The default [GoldenFileComparator] implementation for `flutter test`. @@ -140,7 +165,7 @@ class TrivialComparator implements GoldenFileComparator { /// /// When using `flutter test --update-goldens`, [LocalFileComparator] /// updates the files on disk to match the rendering. -class LocalFileComparator implements GoldenFileComparator { +class LocalFileComparator extends GoldenFileComparator { /// Creates a new [LocalFileComparator] for the specified [testFile]. /// /// Golden file keys will be interpreted as file paths relative to the diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index 9f32de05467..a97299c3e18 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -1712,7 +1712,7 @@ class _MatchesGoldenFile extends AsyncMatcher { imageFuture = _captureImage(elements.single); } - final Uri testNameUri = _getTestNameUri(key, version); + final Uri testNameUri = goldenFileComparator.getTestUri(key, version); final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); return binding.runAsync(() async { @@ -1734,19 +1734,9 @@ class _MatchesGoldenFile extends AsyncMatcher { } @override - Description describe(Description description) => - description.add('one widget whose rasterized image matches golden image "${_getTestNameUri(key, version)}"'); - - Uri _getTestNameUri(Uri key, int version) { - return version == null ? key : Uri.parse( - key - .toString() - .splitMapJoin( - RegExp(r'.png'), - onMatch: (Match m) => '${'.' + version.toString() + m.group(0)}', - onNonMatch: (String n) => '$n' - ) - ); + Description describe(Description description) { + final Uri testNameUri = goldenFileComparator.getTestUri(key, version); + return description.add('one widget whose rasterized image matches golden image "$testNameUri"'); } } diff --git a/packages/flutter_test/test/goldens_test.dart b/packages/flutter_test/test/goldens_test.dart index 5d036428c61..004b33665bb 100644 --- a/packages/flutter_test/test/goldens_test.dart +++ b/packages/flutter_test/test/goldens_test.dart @@ -182,5 +182,18 @@ void main() { expect(fs.file(fix('/foo.png')).readAsBytesSync(), newBytes); }); }); + + group('getTestUri', () { + test('updates file name with version number', () { + final Uri key = Uri.parse('foo.png'); + final Uri key1 = comparator.getTestUri(key, 1); + expect(key1, Uri.parse('foo.1.png')); + }); + test('does nothing for null version number', () { + final Uri key = Uri.parse('foo.png'); + final Uri keyNull = comparator.getTestUri(key, null); + expect(keyNull, Uri.parse('foo.png')); + }); + }); }); } diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index b2108e29ae0..05f59812530 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart @@ -335,30 +335,6 @@ void main() { expect(comparator.imageBytes, hasLength(greaterThan(0))); expect(comparator.golden, Uri.parse('foo.png')); }); - - testWidgets('Comparator succeeds incorporating version number', (WidgetTester tester) async { - await tester.pumpWidget(boilerplate(const Text('hello'))); - final Finder finder = find.byType(Text); - await expectLater(finder, matchesGoldenFile( - 'foo.png', - version: 1, - )); - expect(comparator.invocation, _ComparatorInvocation.compare); - expect(comparator.imageBytes, hasLength(greaterThan(0))); - expect(comparator.golden, Uri.parse('foo.1.png')); - }); - - testWidgets('Comparator succeeds with null version number', (WidgetTester tester) async { - await tester.pumpWidget(boilerplate(const Text('hello'))); - final Finder finder = find.byType(Text); - await expectLater(finder, matchesGoldenFile( - 'foo.png', - version: null, - )); - expect(comparator.invocation, _ComparatorInvocation.compare); - expect(comparator.imageBytes, hasLength(greaterThan(0))); - expect(comparator.golden, Uri.parse('foo.png')); - }); }); group('does not match', () { @@ -413,40 +389,6 @@ void main() { expect(error.message, contains('too many widgets')); } }); - - testWidgets('Comparator failure incorporates version number', (WidgetTester tester) async { - comparator.behavior = _ComparatorBehavior.returnFalse; - await tester.pumpWidget(boilerplate(const Text('hello'))); - final Finder finder = find.byType(Text); - try { - await expectLater(finder, matchesGoldenFile( - 'foo.png', - version: 1, - )); - fail('TestFailure expected but not thrown'); - } on TestFailure catch (error) { - expect(comparator.invocation, _ComparatorInvocation.compare); - expect(error.message, contains('does not match')); - expect(error.message, contains('foo.1.png')); - } - }); - - testWidgets('Comparator failure with null version number', (WidgetTester tester) async { - comparator.behavior = _ComparatorBehavior.returnFalse; - await tester.pumpWidget(boilerplate(const Text('hello'))); - final Finder finder = find.byType(Text); - try { - await expectLater(finder, matchesGoldenFile( - 'foo.png', - version: null, - )); - fail('TestFailure expected but not thrown'); - } on TestFailure catch (error) { - expect(comparator.invocation, _ComparatorInvocation.compare); - expect(error.message, contains('does not match')); - expect(error.message, contains('foo.png')); - } - }); }); testWidgets('calls update on comparator if autoUpdateGoldenFiles is true', (WidgetTester tester) async { @@ -708,6 +650,11 @@ class _FakeComparator implements GoldenFileComparator { this.imageBytes = imageBytes; return Future.value(); } + + @override + Uri getTestUri(Uri key, int version) { + return key; + } } class _FakeSemanticsNode extends SemanticsNode { diff --git a/packages/flutter_tools/dart_test.yaml b/packages/flutter_tools/dart_test.yaml new file mode 100644 index 00000000000..cd8c697cba8 --- /dev/null +++ b/packages/flutter_tools/dart_test.yaml @@ -0,0 +1,5 @@ +tags: + "no_coverage": + "create": + "integration": + diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 42c18082a73..25b39042faa 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -14,6 +14,7 @@ import 'src/build_runner/web_compilation_delegate.dart'; import 'src/codegen.dart'; import 'src/commands/analyze.dart'; +import 'src/commands/assemble.dart'; import 'src/commands/attach.dart'; import 'src/commands/build.dart'; import 'src/commands/channel.dart'; @@ -61,6 +62,7 @@ Future main(List args) async { await runner.run(args, [ AnalyzeCommand(verboseHelp: verboseHelp), + AssembleCommand(), AttachCommand(verboseHelp: verboseHelp), BuildCommand(verboseHelp: verboseHelp), ChannelCommand(verboseHelp: verboseHelp), diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index ff3661adb4d..ef6eb69d69f 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -437,8 +437,8 @@ class AndroidDevice extends Device { DebuggingOptions debuggingOptions, Map platformArgs, bool prebuiltApplication = false, - bool usesTerminalUi = true, bool ipv6 = false, + bool usesTerminalUi = true, }) async { if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion()) return LaunchResult.failed(); diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart index 1c7c17896be..65a8b630f6e 100644 --- a/packages/flutter_tools/lib/src/artifacts.dart +++ b/packages/flutter_tools/lib/src/artifacts.dart @@ -14,10 +14,13 @@ import 'dart/sdk.dart'; import 'globals.dart'; enum Artifact { + /// The tool which compiles a dart kernel file into native code. genSnapshot, + /// The flutter tester binary. flutterTester, snapshotDart, flutterFramework, + /// The framework directory of the macOS desktop. flutterMacOSFramework, vmSnapshotData, isolateSnapshotData, @@ -25,12 +28,24 @@ enum Artifact { platformLibrariesJson, flutterPatchedSdkPath, frontendServerSnapshotForEngineDartSdk, + /// The root directory of the dartk SDK. engineDartSdkPath, + /// The dart binary used to execute any of the required snapshots. engineDartBinary, + /// The dart snapshot of the dart2js compiler. dart2jsSnapshot, + /// The dart snapshot of the dartdev compiler. dartdevcSnapshot, + /// The dart snpashot of the kernel worker compiler. kernelWorkerSnapshot, + /// The root of the web implementation of the dart SDK. flutterWebSdk, + /// The root of the Linux desktop sources. + linuxDesktopPath, + /// The root of the Windows desktop sources. + windowsDesktopPath, + /// The root of the sky_engine package + skyEnginePath, } String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMode mode ]) { @@ -47,6 +62,10 @@ String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMo case Artifact.flutterFramework: return 'Flutter.framework'; case Artifact.flutterMacOSFramework: + if (platform != TargetPlatform.darwin_x64) { + throw Exception('${getNameForTargetPlatform(platform)} does not support' + ' macOS desktop development'); + } return 'FlutterMacOS.framework'; case Artifact.vmSnapshotData: return 'vm_isolate_snapshot.bin'; @@ -74,6 +93,20 @@ String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMo return 'dartdevc.dart.snapshot'; case Artifact.kernelWorkerSnapshot: return 'kernel_worker.dart.snapshot'; + case Artifact.linuxDesktopPath: + if (platform != TargetPlatform.linux_x64) { + throw Exception('${getNameForTargetPlatform(platform)} does not support' + ' Linux desktop development'); + } + return ''; + case Artifact.windowsDesktopPath: + if (platform != TargetPlatform.windows_x64) { + throw Exception('${getNameForTargetPlatform(platform)} does not support' + ' Windows desktop development'); + } + return ''; + case Artifact.skyEnginePath: + return 'sky_engine'; } assert(false, 'Invalid artifact $artifact.'); return null; @@ -209,9 +242,14 @@ class CachedArtifacts extends Artifacts { case Artifact.kernelWorkerSnapshot: return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact)); case Artifact.flutterMacOSFramework: + case Artifact.linuxDesktopPath: + case Artifact.windowsDesktopPath: final String engineArtifactsPath = cache.getArtifactDirectory('engine').path; final String platformDirName = getNameForTargetPlatform(platform); return fs.path.join(engineArtifactsPath, platformDirName, _artifactToFileName(artifact, platform, mode)); + case Artifact.skyEnginePath: + final Directory dartPackageDirectory = cache.getCacheDir('pkg'); + return fs.path.join(dartPackageDirectory.path, _artifactToFileName(artifact)); default: assert(false, 'Artifact $artifact not available for platform $platform.'); return null; @@ -302,6 +340,12 @@ class LocalEngineArtifacts extends Artifacts { return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact)); case Artifact.kernelWorkerSnapshot: return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact)); + case Artifact.linuxDesktopPath: + return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact)); + case Artifact.windowsDesktopPath: + return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact)); + case Artifact.skyEnginePath: + return fs.path.join(_hostEngineOutPath, 'gen', 'dart-pkg', _artifactToFileName(artifact)); } assert(false, 'Invalid artifact $artifact.'); return null; diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart index 8d2338af660..00264f3c572 100644 --- a/packages/flutter_tools/lib/src/base/build.dart +++ b/packages/flutter_tools/lib/src/base/build.dart @@ -9,7 +9,6 @@ import 'package:meta/meta.dart'; import '../artifacts.dart'; import '../build_info.dart'; import '../bundle.dart'; -import '../cache.dart'; import '../compile.dart'; import '../dart/package_map.dart'; import '../globals.dart'; @@ -95,10 +94,6 @@ class AOTSnapshotter { IOSArch iosArch, List extraGenSnapshotOptions = const [], }) async { - FlutterProject flutterProject; - if (fs.file('pubspec.yaml').existsSync()) { - flutterProject = FlutterProject.current(); - } if (!_isValidAotPlatform(platform, buildMode)) { printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.'); return 1; @@ -122,8 +117,6 @@ class AOTSnapshotter { final List inputPaths = [uiPath, vmServicePath, mainPath]; final Set outputPaths = {}; - - final String depfilePath = fs.path.join(outputDir.path, 'snapshot.d'); final List genSnapshotArgs = [ '--deterministic', ]; @@ -165,26 +158,6 @@ class AOTSnapshotter { return 1; } - // If inputs and outputs have not changed since last run, skip the build. - final Fingerprinter fingerprinter = Fingerprinter( - fingerprintPath: '$depfilePath.fingerprint', - paths: [mainPath, ...inputPaths, ...outputPaths], - properties: { - 'buildMode': buildMode.toString(), - 'targetPlatform': platform.toString(), - 'entryPoint': mainPath, - 'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '), - 'engineHash': Cache.instance.engineRevision, - 'buildersUsed': '${flutterProject != null && flutterProject.hasBuilders}', - }, - depfilePaths: [], - ); - // TODO(jonahwilliams): re-enable once this can be proved correct. - // if (await fingerprinter.doesFingerprintMatch()) { - // printTrace('Skipping AOT snapshot build. Fingerprint match.'); - // return 0; - // } - final SnapshotType snapshotType = SnapshotType(platform, buildMode); final int genSnapshotExitCode = await _timedStep('snapshot(CompileTime)', 'aot-snapshot', @@ -210,9 +183,6 @@ class AOTSnapshotter { if (result.exitCode != 0) return result.exitCode; } - - // Compute and record build fingerprint. - await fingerprinter.writeFingerprint(); return 0; } diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 87361354e5c..ac0489a3638 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -115,6 +115,32 @@ enum BuildMode { release, } +const List _kBuildModes = [ + 'debug', + 'profile', + 'release', + 'dynamic-profile', + 'dynamic-release', +]; + +/// Return the name for the build mode, or "any" if null. +String getNameForBuildMode(BuildMode buildMode) { + return _kBuildModes[buildMode.index]; +} + +/// Returns the [BuildMode] for a particular `name`. +BuildMode getBuildModeForName(String name) { + switch (name) { + case 'debug': + return BuildMode.debug; + case 'profile': + return BuildMode.profile; + case 'release': + return BuildMode.release; + } + return null; +} + String validatedBuildNumberForPlatform(TargetPlatform targetPlatform, String buildNumber) { if (buildNumber == null) { return null; diff --git a/packages/flutter_tools/lib/src/build_system/build_system.dart b/packages/flutter_tools/lib/src/build_system/build_system.dart new file mode 100644 index 00000000000..c0f9d224e2e --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/build_system.dart @@ -0,0 +1,686 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; +import 'package:meta/meta.dart'; +import 'package:pool/pool.dart'; + +import '../base/file_system.dart'; +import '../base/platform.dart'; +import '../cache.dart'; +import '../convert.dart'; +import '../globals.dart'; +import 'exceptions.dart'; +import 'file_hash_store.dart'; +import 'source.dart'; +import 'targets/assets.dart'; +import 'targets/dart.dart'; +import 'targets/ios.dart'; +import 'targets/linux.dart'; +import 'targets/macos.dart'; +import 'targets/windows.dart'; + +export 'source.dart'; + +/// The function signature of a build target which can be invoked to perform +/// the underlying task. +typedef BuildAction = FutureOr Function( + Map inputs, Environment environment); + +/// A description of the update to each input file. +enum ChangeType { + /// The file was added. + Added, + /// The file was deleted. + Removed, + /// The file was modified. + Modified, +} + +/// Configuration for the build system itself. +class BuildSystemConfig { + /// Create a new [BuildSystemConfig]. + const BuildSystemConfig({this.resourcePoolSize}); + + /// The maximum number of concurrent tasks the build system will run. + /// + /// If not provided, defaults to [platform.numberOfProcessors]. + final int resourcePoolSize; +} + +/// A Target describes a single step during a flutter build. +/// +/// The target inputs are required to be files discoverable via a combination +/// of at least one of the environment values and zero or more local values. +/// +/// To determine if the action for a target needs to be executed, the +/// [BuildSystem] performs a hash of the file contents for both inputs and +/// outputs. This is tracked separately in the [FileHashStore]. +/// +/// A Target has both implicit and explicit inputs and outputs. Only the +/// later are safe to evaluate before invoking the [buildAction]. For example, +/// a wildcard output pattern requires the outputs to exist before it can +/// glob files correctly. +/// +/// - All listed inputs are considered explicit inputs. +/// - Outputs which are provided as [Source.pattern]. +/// without wildcards are considered explicit. +/// - The remaining outputs are considered implicit. +/// +/// For each target, executing its action creates a corresponding stamp file +/// which records both the input and output files. This file is read by +/// subsequent builds to determine which file hashes need to be checked. If the +/// stamp file is missing, the target's action is always rerun. +/// +/// file: `example_target.stamp` +/// +/// { +/// "inputs": [ +/// "absolute/path/foo", +/// "absolute/path/bar", +/// ... +/// ], +/// "outputs": [ +/// "absolute/path/fizz" +/// ] +/// } +/// +/// ## Code review +/// +/// ### Targes should only depend on files that are provided as inputs +/// +/// Example: gen_snapshot must be provided as an input to the aot_elf +/// build steps, even though it isn't a source file. This ensures that changes +/// to the gen_snapshot binary (during a local engine build) correctly +/// trigger a corresponding build update. +/// +/// Example: aot_elf has a dependency on the dill and packages file +/// produced by the kernel_snapshot step. +/// +/// ### Targest should declare all outputs produced +/// +/// If a target produces an output it should be listed, even if it is not +/// intended to be consumed by another target. +/// +/// ## Unit testing +/// +/// Most targets will invoke an external binary which makes unit testing +/// trickier. It is recommend that for unit testing that a Fake is used and +/// provided via the dependency injection system. a [Testbed] may be used to +/// set up the environment before the test is run. Unit tests should fully +/// exercise the rule, ensuring that the existing input and output verification +/// logic can run, as well as verifying it correctly handles provided defines +/// and meets any additional contracts present in the target. +class Target { + const Target({ + @required this.name, + @required this.inputs, + @required this.outputs, + @required this.buildAction, + this.dependencies = const [], + }); + + /// The user-readable name of the target. + /// + /// This information is surfaced in the assemble commands and used as an + /// argument to build a particular target. + final String name; + + /// The dependencies of this target. + final List dependencies; + + /// The input [Source]s which are diffed to determine if a target should run. + final List inputs; + + /// The output [Source]s which we attempt to verify are correctly produced. + final List outputs; + + /// The action which performs this build step. + final BuildAction buildAction; + + /// Collect hashes for all inputs to determine if any have changed. + Future> computeChanges( + List inputs, + Environment environment, + FileHashStore fileHashStore, + ) async { + final Map updates = {}; + final File stamp = _findStampFile(environment); + final Set previousInputs = {}; + final List previousOutputs = []; + + // If the stamp file doesn't exist, we haven't run this step before and + // all inputs were added. + if (stamp.existsSync()) { + final String content = stamp.readAsStringSync(); + // Something went wrong writing the stamp file. + if (content == null || content.isEmpty) { + stamp.deleteSync(); + } else { + final Map values = json.decode(content); + final List inputs = values['inputs']; + final List outputs = values['outputs']; + inputs.cast().forEach(previousInputs.add); + outputs.cast().forEach(previousOutputs.add); + } + } + + // For each input type, first determine if we've already computed the hash + // for it. If not and it is a directory we skip hashing and instead use a + // timestamp. If it is a file we collect it to be sent off for hashing as + // a group. + final List sourcesToHash = []; + final List missingInputs = []; + for (File file in inputs) { + if (!file.existsSync()) { + missingInputs.add(file); + continue; + } + + final String absolutePath = file.resolveSymbolicLinksSync(); + final String previousHash = fileHashStore.previousHashes[absolutePath]; + if (fileHashStore.currentHashes.containsKey(absolutePath)) { + final String currentHash = fileHashStore.currentHashes[absolutePath]; + if (currentHash != previousHash) { + updates[absolutePath] = previousInputs.contains(absolutePath) + ? ChangeType.Modified + : ChangeType.Added; + } + } else { + sourcesToHash.add(file); + } + } + // Check if any outputs were deleted or modified from the previous run. + for (String previousOutput in previousOutputs) { + final File file = fs.file(previousOutput); + if (!file.existsSync()) { + updates[previousOutput] = ChangeType.Removed; + continue; + } + final String absolutePath = file.resolveSymbolicLinksSync(); + final String previousHash = fileHashStore.previousHashes[absolutePath]; + if (fileHashStore.currentHashes.containsKey(absolutePath)) { + final String currentHash = fileHashStore.currentHashes[absolutePath]; + if (currentHash != previousHash) { + updates[absolutePath] = previousInputs.contains(absolutePath) + ? ChangeType.Modified + : ChangeType.Added; + } + } else { + sourcesToHash.add(file); + } + } + + if (missingInputs.isNotEmpty) { + throw MissingInputException(missingInputs, name); + } + + // If we have files to hash, compute them asynchronously and then + // update the result. + if (sourcesToHash.isNotEmpty) { + final List dirty = await fileHashStore.hashFiles(sourcesToHash); + for (File file in dirty) { + final String absolutePath = file.resolveSymbolicLinksSync(); + updates[absolutePath] = previousInputs.contains(absolutePath) + ? ChangeType.Modified + : ChangeType.Added; + } + } + + // Find which, if any, inputs have been deleted. + final Set currentInputPaths = Set.from( + inputs.map((File entity) => entity.resolveSymbolicLinksSync()) + ); + for (String previousInput in previousInputs) { + if (!currentInputPaths.contains(previousInput)) { + updates[previousInput] = ChangeType.Removed; + } + } + return updates; + } + + /// Invoke to remove the stamp file if the [buildAction] threw an exception; + void clearStamp(Environment environment) { + final File stamp = _findStampFile(environment); + if (stamp.existsSync()) { + stamp.deleteSync(); + } + } + + void _writeStamp( + List inputs, + List outputs, + Environment environment, + ) { + final File stamp = _findStampFile(environment); + final List inputPaths = []; + for (File input in inputs) { + inputPaths.add(input.resolveSymbolicLinksSync()); + } + final List outputPaths = []; + for (File output in outputs) { + outputPaths.add(output.resolveSymbolicLinksSync()); + } + final Map result = { + 'inputs': inputPaths, + 'outputs': outputPaths, + }; + if (!stamp.existsSync()) { + stamp.createSync(); + } + stamp.writeAsStringSync(json.encode(result)); + } + + /// Resolve the set of input patterns and functions into a concrete list of + /// files. + List resolveInputs( + Environment environment, + ) { + return _resolveConfiguration(inputs, environment, implicit: true, inputs: true); + } + + /// Find the current set of declared outputs, including wildcard directories. + /// + /// The [implicit] flag controls whether it is safe to evaluate [Source]s + /// which uses functions, behaviors, or patterns. + List resolveOutputs( + Environment environment, + { bool implicit = true, } + ) { + final List outputEntities = _resolveConfiguration(outputs, environment, implicit: implicit, inputs: false); + if (implicit) { + verifyOutputDirectories(outputEntities, environment, this); + } + return outputEntities; + } + + /// Performs a fold across this target and its dependencies. + T fold(T initialValue, T combine(T previousValue, Target target)) { + final T dependencyResult = dependencies.fold( + initialValue, (T prev, Target t) => t.fold(prev, combine)); + return combine(dependencyResult, this); + } + + /// Convert the target to a JSON structure appropriate for consumption by + /// external systems. + /// + /// This requires constants from the [Environment] to resolve the paths of + /// inputs and the output stamp. + Map toJson(Environment environment) { + return { + 'name': name, + 'dependencies': dependencies.map((Target target) => target.name).toList(), + 'inputs': resolveInputs(environment) + .map((File file) => file.resolveSymbolicLinksSync()) + .toList(), + 'outputs': resolveOutputs(environment, implicit: false) + .map((File file) => file.path) + .toList(), + 'stamp': _findStampFile(environment).absolute.path, + }; + } + + /// Locate the stamp file for a particular target name and environment. + File _findStampFile(Environment environment) { + final String fileName = '$name.stamp'; + return environment.buildDir.childFile(fileName); + } + + static List _resolveConfiguration( + List config, Environment environment, { bool implicit = true, bool inputs = true }) { + final SourceVisitor collector = SourceVisitor(environment, inputs); + for (Source source in config) { + source.accept(collector); + } + return collector.sources; + } +} + +/// The [Environment] defines several constants for use during the build. +/// +/// The environment contains configuration and file paths that are safe to +/// depend on and reference during the build. +/// +/// Example (Good): +/// +/// Use the environment to determine where to write an output file. +/// +/// environment.buildDir.childFile('output') +/// ..createSync() +/// ..writeAsStringSync('output data'); +/// +/// Example (Bad): +/// +/// Use a hard-coded path or directory relative to the current working +/// directory to write an output file. +/// +/// fs.file('build/linux/out') +/// ..createSync() +/// ..writeAsStringSync('output data'); +/// +/// Example (Good): +/// +/// Using the build mode to produce different output. Note that the action +/// is still responsible for outputting a different file, as defined by the +/// corresponding output [Source]. +/// +/// final BuildMode buildMode = getBuildModeFromDefines(environment.defines); +/// if (buildMode == BuildMode.debug) { +/// environment.buildDir.childFile('debug.output') +/// ..createSync() +/// ..writeAsStringSync('debug'); +/// } else { +/// environment.buildDir.childFile('non_debug.output') +/// ..createSync() +/// ..writeAsStringSync('non_debug'); +/// } +class Environment { + /// Create a new [Environment] object. + /// + /// Only [projectDir] is required. The remaining environment locations have + /// defaults based on it. + factory Environment({ + @required Directory projectDir, + Directory buildDir, + Map defines = const {}, + }) { + // Compute a unique hash of this build's particular environment. + // Sort the keys by key so that the result is stable. We always + // include the engine and dart versions. + String buildPrefix; + final List keys = defines.keys.toList()..sort(); + final StringBuffer buffer = StringBuffer(); + for (String key in keys) { + buffer.write(key); + buffer.write(defines[key]); + } + // in case there was no configuration, provide some value. + buffer.write('Flutter is awesome'); + final String output = buffer.toString(); + final Digest digest = md5.convert(utf8.encode(output)); + buildPrefix = hex.encode(digest.bytes); + + final Directory rootBuildDir = buildDir ?? projectDir.childDirectory('build'); + final Directory buildDirectory = rootBuildDir.childDirectory(buildPrefix); + return Environment._( + projectDir: projectDir, + buildDir: buildDirectory, + rootBuildDir: rootBuildDir, + cacheDir: Cache.instance.getRoot(), + defines: defines, + ); + } + + Environment._({ + @required this.projectDir, + @required this.buildDir, + @required this.rootBuildDir, + @required this.cacheDir, + @required this.defines, + }); + + /// The [Source] value which is substituted with the path to [projectDir]. + static const String kProjectDirectory = '{PROJECT_DIR}'; + + /// The [Source] value which is substituted with the path to [buildDir]. + static const String kBuildDirectory = '{BUILD_DIR}'; + + /// The [Source] value which is substituted with the path to [cacheDir]. + static const String kCacheDirectory = '{CACHE_DIR}'; + + /// The [Source] value which is substituted with a path to the flutter root. + static const String kFlutterRootDirectory = '{FLUTTER_ROOT}'; + + /// The `PROJECT_DIR` environment variable. + /// + /// This should be root of the flutter project where a pubspec and dart files + /// can be located. + final Directory projectDir; + + /// The `BUILD_DIR` environment variable. + /// + /// Defaults to `{PROJECT_ROOT}/build`. The root of the output directory where + /// build step intermediates and outputs are written. + final Directory buildDir; + + /// The `CACHE_DIR` environment variable. + /// + /// Defaults to `{FLUTTER_ROOT}/bin/cache`. The root of the artifact cache for + /// the flutter tool. + final Directory cacheDir; + + /// Additional configuration passed to the build targets. + /// + /// Setting values here forces a unique build directory to be chosen + /// which prevents the config from leaking into different builds. + final Map defines; + + /// The root build directory shared by all builds. + final Directory rootBuildDir; +} + +/// The result information from the build system. +class BuildResult { + BuildResult(this.success, this.exceptions, this.performance); + + final bool success; + final Map exceptions; + final Map performance; + + bool get hasException => exceptions.isNotEmpty; +} + +/// The build system is responsible for invoking and ordering [Target]s. +class BuildSystem { + BuildSystem([Map targets]) + : targets = targets ?? _defaultTargets; + + /// All currently registered targets. + static final Map _defaultTargets = { + unpackMacos.name: unpackMacos, + macosApplication.name: macosApplication, + macoReleaseApplication.name: macoReleaseApplication, + unpackLinux.name: unpackLinux, + unpackWindows.name: unpackWindows, + copyAssets.name: copyAssets, + kernelSnapshot.name: kernelSnapshot, + aotElfProfile.name: aotElfProfile, + aotElfRelease.name: aotElfRelease, + aotAssemblyProfile.name: aotAssemblyProfile, + aotAssemblyRelease.name: aotAssemblyRelease, + releaseIosApplication.name: releaseIosApplication, + profileIosApplication.name: profileIosApplication, + debugIosApplication.name: debugIosApplication, + }; + + final Map targets; + + /// Build the target `name` and all of its dependencies. + Future build( + String name, + Environment environment, + BuildSystemConfig buildSystemConfig, + ) async { + final Target target = _getNamedTarget(name); + environment.buildDir.createSync(recursive: true); + + // Load file hash store from previous builds. + final FileHashStore fileCache = FileHashStore(environment) + ..initialize(); + + // Perform sanity checks on build. + checkCycles(target); + + final _BuildInstance buildInstance = _BuildInstance(environment, fileCache, buildSystemConfig); + bool passed = true; + try { + passed = await buildInstance.invokeTarget(target); + } finally { + // Always persist the file cache to disk. + fileCache.persist(); + } + return BuildResult( + passed, + buildInstance.exceptionMeasurements, + buildInstance.stepTimings, + ); + } + + /// Describe the target `name` and all of its dependencies. + List> describe( + String name, + Environment environment, + ) { + final Target target = _getNamedTarget(name); + environment.buildDir.createSync(recursive: true); + checkCycles(target); + // Cheat a bit and re-use the same map. + Map> fold(Map> accumulation, Target current) { + accumulation[current.name] = current.toJson(environment); + return accumulation; + } + + final Map> result = + >{}; + final Map> targets = target.fold(result, fold); + return targets.values.toList(); + } + + // Returns the corresponding target or throws. + Target _getNamedTarget(String name) { + final Target target = targets[name]; + if (target == null) { + throw Exception('No registered target:$name.'); + } + return target; + } +} + +/// An active instance of a build. +class _BuildInstance { + _BuildInstance(this.environment, this.fileCache, this.buildSystemConfig) + : resourcePool = Pool(buildSystemConfig.resourcePoolSize ?? platform?.numberOfProcessors ?? 1); + + final BuildSystemConfig buildSystemConfig; + final Pool resourcePool; + final Map> pending = >{}; + final Environment environment; + final FileHashStore fileCache; + + // Timings collected during target invocation. + final Map stepTimings = {}; + + // Exceptions caught during the build process. + final Map exceptionMeasurements = {}; + + Future invokeTarget(Target target) async { + final List results = await Future.wait(target.dependencies.map(invokeTarget)); + if (results.any((bool result) => !result)) { + return false; + } + final AsyncMemoizer memoizer = pending[target.name] ??= AsyncMemoizer(); + return memoizer.runOnce(() => _invokeInternal(target)); + } + + Future _invokeInternal(Target target) async { + final PoolResource resource = await resourcePool.request(); + final Stopwatch stopwatch = Stopwatch()..start(); + bool passed = true; + bool skipped = false; + try { + final List inputs = target.resolveInputs(environment); + final Map updates = await target.computeChanges(inputs, environment, fileCache); + if (updates.isEmpty) { + skipped = true; + printStatus('Skipping target: ${target.name}'); + } else { + printStatus('${target.name}: Starting'); + // build actions may be null. + await target?.buildAction(updates, environment); + printStatus('${target.name}: Complete'); + + final List outputs = target.resolveOutputs(environment); + // Update hashes for output files. + await fileCache.hashFiles(outputs); + target._writeStamp(inputs, outputs, environment); + } + } catch (exception, stackTrace) { + // TODO(jonahwilliams): test + target.clearStamp(environment); + passed = false; + skipped = false; + exceptionMeasurements[target.name] = ExceptionMeasurement( + target.name, exception, stackTrace); + } finally { + resource.release(); + stopwatch.stop(); + stepTimings[target.name] = PerformanceMeasurement( + target.name, stopwatch.elapsedMilliseconds, skipped, passed); + } + return passed; + } +} + +/// Helper class to collect exceptions. +class ExceptionMeasurement { + ExceptionMeasurement(this.target, this.exception, this.stackTrace); + + final String target; + final dynamic exception; + final StackTrace stackTrace; +} + +/// Helper class to collect measurement data. +class PerformanceMeasurement { + PerformanceMeasurement(this.target, this.elapsedMilliseconds, this.skiped, this.passed); + final int elapsedMilliseconds; + final String target; + final bool skiped; + final bool passed; +} + +/// Check if there are any dependency cycles in the target. +/// +/// Throws a [CycleException] if one is encountered. +void checkCycles(Target initial) { + void checkInternal(Target target, Set visited, Set stack) { + if (stack.contains(target)) { + throw CycleException(stack..add(target)); + } + if (visited.contains(target)) { + return; + } + visited.add(target); + stack.add(target); + for (Target dependency in target.dependencies) { + checkInternal(dependency, visited, stack); + } + stack.remove(target); + } + checkInternal(initial, {}, {}); +} + +/// Verifies that all files exist and are in a subdirectory of [Environment.buildDir]. +void verifyOutputDirectories(List outputs, Environment environment, Target target) { + final String buildDirectory = environment.buildDir.resolveSymbolicLinksSync(); + final String projectDirectory = environment.projectDir.resolveSymbolicLinksSync(); + final List missingOutputs = []; + for (File sourceFile in outputs) { + if (!sourceFile.existsSync()) { + missingOutputs.add(sourceFile); + continue; + } + final String path = sourceFile.resolveSymbolicLinksSync(); + if (!path.startsWith(buildDirectory) && !path.startsWith(projectDirectory)) { + throw MisplacedOutputException(path, target.name); + } + } + if (missingOutputs.isNotEmpty) { + throw MissingOutputException(missingOutputs, target.name); + } +} diff --git a/packages/flutter_tools/lib/src/build_system/exceptions.dart b/packages/flutter_tools/lib/src/build_system/exceptions.dart new file mode 100644 index 00000000000..918c0517814 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/exceptions.dart @@ -0,0 +1,95 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../base/file_system.dart'; + +import 'build_system.dart'; + +/// An exception thrown when a rule declares an input that does not exist on +/// disk. +class MissingInputException implements Exception { + const MissingInputException(this.missing, this.target); + + /// The file or directory we expected to find. + final List missing; + + /// The name of the target this file should have been output from. + final String target; + + @override + String toString() { + final String files = missing.map((File file) => file.path).join(', '); + return '$files were declared as an inputs, but did not exist. ' + 'Check the definition of target:$target for errors'; + } +} + +/// An exception thrown if we detect a cycle in the dependencies of a target. +class CycleException implements Exception { + CycleException(this.targets); + + final Set targets; + + @override + String toString() => 'Dependency cycle detected in build: ' + '${targets.map((Target target) => target.name).join(' -> ')}'; +} + +/// An exception thrown when a pattern is invalid. +class InvalidPatternException implements Exception { + InvalidPatternException(this.pattern); + + final String pattern; + + @override + String toString() => 'The pattern "$pattern" is not valid'; +} + +/// An exception thrown when a rule declares an output that was not produced +/// by the invocation. +class MissingOutputException implements Exception { + const MissingOutputException(this.missing, this.target); + + /// The files we expected to find. + final List missing; + + /// The name of the target this file should have been output from. + final String target; + + @override + String toString() { + final String files = missing.map((File file) => file.path).join(', '); + return '$files were declared as outputs, but were not generated by ' + 'the action. Check the definition of target:$target for errors'; + } +} + +/// An exception thrown when in output is placed outside of +/// [Environment.buildDir]. +class MisplacedOutputException implements Exception { + MisplacedOutputException(this.path, this.target); + + final String path; + final String target; + + @override + String toString() { + return 'Target $target produced an output at $path' + ' which is outside of the current build or project directory'; + } +} + +/// An exception thrown if a build action is missing a required define. +class MissingDefineException implements Exception { + MissingDefineException(this.define, this.target); + + final String define; + final String target; + + @override + String toString() { + return 'Target $target required define $define ' + 'but it was not provided'; + } +} diff --git a/packages/flutter_tools/lib/src/build_system/file_hash_store.dart b/packages/flutter_tools/lib/src/build_system/file_hash_store.dart new file mode 100644 index 00000000000..398f95f45e7 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/file_hash_store.dart @@ -0,0 +1,111 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:collection'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; + +import '../base/file_system.dart'; +import '../globals.dart'; +import 'build_system.dart'; +import 'filecache.pb.dart' as pb; + +/// A globally accessible cache of file hashes. +/// +/// In cases where multiple targets read the same source files as inputs, we +/// avoid recomputing or storing multiple copies of hashes by delegating +/// through this class. All file hashes are held in memory during a build +/// operation, and persisted to cache in the root build directory. +/// +/// The format of the file store is subject to change and not part of its API. +/// +/// To regenerate the protobuf entries used to construct the cache: +/// 1. If not already installed, https://developers.google.com/protocol-buffers/docs/downloads +/// 2. pub global active `protoc-gen-dart` +/// 3. protoc -I=lib/src/build_system/ --dart_out=lib/src/build_system/ lib/src/build_system/filecache.proto +/// 4. Add licenses headers to the newly generated file and check-in. +/// +/// See also: https://developers.google.com/protocol-buffers/docs/darttutorial +// TODO(jonahwilliams): find a better way to clear out old entries, perhaps +// track the last access or modification date? +class FileHashStore { + FileHashStore(this.environment); + + final Environment environment; + final HashMap previousHashes = HashMap(); + final HashMap currentHashes = HashMap(); + + // The name of the file which stores the file hashes. + static const String _kFileCache = '.filecache'; + + // The current version of the file cache storage format. + static const int _kVersion = 1; + + /// Read file hashes from disk. + void initialize() { + printTrace('Initializing file store'); + if (!_cacheFile.existsSync()) { + return; + } + final List data = _cacheFile.readAsBytesSync(); + final pb.FileStorage fileStorage = pb.FileStorage.fromBuffer(data); + if (fileStorage.version != _kVersion) { + _cacheFile.deleteSync(); + return; + } + for (pb.FileHash fileHash in fileStorage.files) { + previousHashes[fileHash.path] = fileHash.hash; + } + printTrace('Done initializing file store'); + } + + /// Persist file hashes to disk. + void persist() { + printTrace('Persisting file store'); + final pb.FileStorage fileStorage = pb.FileStorage(); + fileStorage.version = _kVersion; + final File file = _cacheFile; + if (!file.existsSync()) { + file.createSync(); + } + for (MapEntry entry in currentHashes.entries) { + previousHashes[entry.key] = entry.value; + } + for (MapEntry entry in previousHashes.entries) { + final pb.FileHash fileHash = pb.FileHash(); + fileHash.path = entry.key; + fileHash.hash = entry.value; + fileStorage.files.add(fileHash); + } + final Uint8List buffer = fileStorage.writeToBuffer(); + file.writeAsBytesSync(buffer); + printTrace('Done persisting file store'); + } + + /// Computes a hash of the provided files and returns a list of entities + /// that were dirty. + // TODO(jonahwilliams): compare hash performance with md5 tool on macOS and + // linux and certutil on Windows, as well as dividing up computation across + // isolates. This also related to the current performance issue with checking + // APKs before installing them on device. + Future> hashFiles(List files) async { + final List dirty = []; + for (File file in files) { + final String absolutePath = file.resolveSymbolicLinksSync(); + final String previousHash = previousHashes[absolutePath]; + final List bytes = file.readAsBytesSync(); + final String currentHash = md5.convert(bytes).toString(); + + if (currentHash != previousHash) { + dirty.add(file); + } + currentHashes[absolutePath] = currentHash; + } + return dirty; + } + + File get _cacheFile => environment.rootBuildDir.childFile(_kFileCache); +} diff --git a/packages/flutter_tools/lib/src/build_system/filecache.pb.dart b/packages/flutter_tools/lib/src/build_system/filecache.pb.dart new file mode 100644 index 00000000000..4c095acef54 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/filecache.pb.dart @@ -0,0 +1,96 @@ +// 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. + +/// +// Generated code. Do not modify. +// source: lib/src/build_system/filecache.proto +/// +// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name, sort_constructors_first + +import 'dart:core' as $core show bool, Deprecated, double, int, List, Map, override, pragma, String, dynamic; + +import 'package:protobuf/protobuf.dart' as $pb; + +class FileHash extends $pb.GeneratedMessage { + factory FileHash() => create(); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo('FileHash', package: const $pb.PackageName('flutter_tools')) + ..aOS(1, 'path') + ..aOS(2, 'hash') + ..hasRequiredFields = false; + + FileHash._() : super(); + + factory FileHash.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + + factory FileHash.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + @$core.override + FileHash clone() => FileHash()..mergeFromMessage(this); + + @$core.override + FileHash copyWith(void Function(FileHash) updates) => super.copyWith(($core.dynamic message) => updates(message as FileHash)); + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static FileHash create() => FileHash._(); + + @$core.override + FileHash createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + static FileHash getDefault() => _defaultInstance ??= create()..freeze(); + static FileHash _defaultInstance; + + $core.String get path => $_getS(0, ''); + set path($core.String v) { $_setString(0, v); } + $core.bool hasPath() => $_has(0); + void clearPath() => clearField(1); + + $core.String get hash => $_getS(1, ''); + set hash($core.String v) { $_setString(1, v); } + $core.bool hasHash() => $_has(1); + void clearHash() => clearField(2); +} + +class FileStorage extends $pb.GeneratedMessage { + factory FileStorage() => create(); + static final $pb.BuilderInfo _i = $pb.BuilderInfo('FileHashStore', package: const $pb.PackageName('flutter_tools')) + ..a<$core.int>(1, 'version', $pb.PbFieldType.O3) + ..pc(2, 'files', $pb.PbFieldType.PM,FileHash.create) + ..hasRequiredFields = false; + + FileStorage._() : super(); + factory FileStorage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory FileStorage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + @$core.override + FileStorage clone() => FileStorage()..mergeFromMessage(this); + + @$core.override + FileStorage copyWith(void Function(FileStorage) updates) => super.copyWith(($core.dynamic message) => updates(message as FileStorage)); + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static FileStorage create() => FileStorage._(); + + @$core.override + FileStorage createEmptyInstance() => create(); + + static $pb.PbList createRepeated() => $pb.PbList(); + + static FileStorage getDefault() => _defaultInstance ??= create()..freeze(); + + static FileStorage _defaultInstance; + + $core.int get version => $_get(0, 0); + set version($core.int v) { $_setSignedInt32(0, v); } + $core.bool hasVersion() => $_has(0); + void clearVersion() => clearField(1); + + $core.List get files => $_getList(1); +} diff --git a/packages/flutter_tools/lib/src/build_system/filecache.pbjson.dart b/packages/flutter_tools/lib/src/build_system/filecache.pbjson.dart new file mode 100644 index 00000000000..32f6f9eafc9 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/filecache.pbjson.dart @@ -0,0 +1,25 @@ +// 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. + +/// +// Generated code. Do not modify. +// source: lib/src/build_system/filecache.proto +/// +// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name + +const Map FileHash$json = { + '1': 'FileHash', + '2': >[ + {'1': 'path', '3': 1, '4': 1, '5': 9, '10': 'path'}, + {'1': 'hash', '3': 2, '4': 1, '5': 9, '10': 'hash'}, + ], +}; + +const Map FileStorage$json = { + '1': 'FileHashStore', + '2': >[ + {'1': 'version', '3': 1, '4': 1, '5': 5, '10': 'version'}, + {'1': 'files', '3': 2, '4': 3, '5': 11, '6': '.flutter_tools.FileHash', '10': 'files'}, + ], +}; diff --git a/packages/flutter_tools/lib/src/build_system/filecache.proto b/packages/flutter_tools/lib/src/build_system/filecache.proto new file mode 100644 index 00000000000..63be878f4fc --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/filecache.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +package flutter_tools; + +message FileHash { + // The absolute path to the file on disk. + string path = 1; + + // The last computed file hash. + string hash = 2; +} + +message FileStorage { + // The current version of the file store. + int32 version = 1; + + // All currently stored files. + repeated FileHash files = 2; +} diff --git a/packages/flutter_tools/lib/src/build_system/source.dart b/packages/flutter_tools/lib/src/build_system/source.dart new file mode 100644 index 00000000000..e9f1e1642cb --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/source.dart @@ -0,0 +1,228 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../artifacts.dart'; +import '../base/file_system.dart'; +import '../build_info.dart'; +import '../globals.dart'; +import 'build_system.dart'; +import 'exceptions.dart'; + +/// An input function produces a list of additional input files for an +/// [Environment]. +typedef InputFunction = List Function(Environment environment); + +/// Collects sources for a [Target] into a single list of [FileSystemEntities]. +class SourceVisitor { + /// Create a new [SourceVisitor] from an [Environment]. + SourceVisitor(this.environment, [this.inputs = true]); + + /// The current environment. + final Environment environment; + + /// Whether we are visiting inputs or outputs. + /// + /// Defaults to `true`. + final bool inputs; + + /// The entities are populated after visiting each source. + final List sources = []; + + /// Visit a [Source] which contains a function. + /// + /// The function is expected to produce a list of [FileSystemEntities]s. + void visitFunction(InputFunction function) { + sources.addAll(function(environment)); + } + + /// Visit a [Source] which contains a file uri. + /// + /// The uri may that may include constants defined in an [Environment]. + void visitPattern(String pattern) { + // perform substitution of the environmental values and then + // of the local values. + final List segments = []; + final List rawParts = pattern.split('/'); + final bool hasWildcard = rawParts.last.contains('*'); + String wildcardFile; + if (hasWildcard) { + wildcardFile = rawParts.removeLast(); + } + // If the pattern does not start with an env variable, then we have nothing + // to resolve it to, error out. + switch (rawParts.first) { + case Environment.kProjectDirectory: + segments.addAll( + fs.path.split(environment.projectDir.resolveSymbolicLinksSync())); + break; + case Environment.kBuildDirectory: + segments.addAll(fs.path.split( + environment.buildDir.resolveSymbolicLinksSync())); + break; + case Environment.kCacheDirectory: + segments.addAll( + fs.path.split(environment.cacheDir.resolveSymbolicLinksSync())); + break; + case Environment.kFlutterRootDirectory: + segments.addAll( + fs.path.split(environment.cacheDir.resolveSymbolicLinksSync())); + break; + default: + throw InvalidPatternException(pattern); + } + rawParts.skip(1).forEach(segments.add); + final String filePath = fs.path.joinAll(segments); + if (hasWildcard) { + // Perform a simple match by splitting the wildcard containing file one + // the `*`. For example, for `/*.dart`, we get [.dart]. We then check + // that part of the file matches. If there are values before and after + // the `*` we need to check that both match without overlapping. For + // example, `foo_*_.dart`. We want to match `foo_b_.dart` but not + // `foo_.dart`. To do so, we first subtract the first section from the + // string if the first segment matches. + final List segments = wildcardFile.split('*'); + if (segments.length > 2) { + throw InvalidPatternException(pattern); + } + if (!fs.directory(filePath).existsSync()) { + throw Exception('$filePath does not exist!'); + } + for (FileSystemEntity entity in fs.directory(filePath).listSync()) { + final String filename = fs.path.basename(entity.path); + if (segments.isEmpty) { + sources.add(fs.file(entity.absolute)); + } else if (segments.length == 1) { + if (filename.startsWith(segments[0]) || + filename.endsWith(segments[0])) { + sources.add(entity.absolute); + } + } else if (filename.startsWith(segments[0])) { + if (filename.substring(segments[0].length).endsWith(segments[1])) { + sources.add(entity.absolute); + } + } + } + } else { + sources.add(fs.file(fs.path.normalize(filePath))); + } + } + + /// Visit a [Source] which contains a [SourceBehavior]. + void visitBehavior(SourceBehavior sourceBehavior) { + if (inputs) { + sources.addAll(sourceBehavior.inputs(environment)); + } else { + sources.addAll(sourceBehavior.outputs(environment)); + } + } + + /// Visit a [Source] which is defined by an [Artifact] from the flutter cache. + /// + /// If the [Artifact] points to a directory then all child files are included. + void visitArtifact(Artifact artifact, TargetPlatform platform, BuildMode mode) { + final String path = artifacts.getArtifactPath(artifact, platform: platform, mode: mode); + if (fs.isDirectorySync(path)) { + sources.addAll([ + for (FileSystemEntity entity in fs.directory(path).listSync(recursive: true)) + if (entity is File) + entity + ]); + } else { + sources.add(fs.file(path)); + } + } +} + +/// A description of an input or output of a [Target]. +abstract class Source { + /// This source is a file-uri which contains some references to magic + /// environment variables. + const factory Source.pattern(String pattern) = _PatternSource; + + /// This source is produced by invoking the provided function. + const factory Source.function(InputFunction function) = _FunctionSource; + + /// This source is produced by the [SourceBehavior] class. + const factory Source.behavior(SourceBehavior behavior) = _SourceBehavior; + + /// The source is provided by an [Artifact]. + /// + /// If [artifact] points to a directory then all child files are included. + const factory Source.artifact(Artifact artifact, {TargetPlatform platform, + BuildMode mode}) = _ArtifactSource; + + /// Visit the particular source type. + void accept(SourceVisitor visitor); + + /// Whether the output source provided can be known before executing the rule. + /// + /// This does not apply to inputs, which are always explicit and must be + /// evaluated before the build. + /// + /// For example, [Source.pattern] and [Source.version] are not implicit + /// provided they do not use any wildcards. [Source.behavior] and + /// [Source.function] are always implicit. + bool get implicit; +} + +/// An interface for describing input and output copies together. +abstract class SourceBehavior { + const SourceBehavior(); + + /// The inputs for a particular target. + List inputs(Environment environment); + + /// The outputs for a particular target. + List outputs(Environment environment); +} + +class _SourceBehavior implements Source { + const _SourceBehavior(this.value); + + final SourceBehavior value; + + @override + void accept(SourceVisitor visitor) => visitor.visitBehavior(value); + + @override + bool get implicit => true; +} + +class _FunctionSource implements Source { + const _FunctionSource(this.value); + + final InputFunction value; + + @override + void accept(SourceVisitor visitor) => visitor.visitFunction(value); + + @override + bool get implicit => true; +} + +class _PatternSource implements Source { + const _PatternSource(this.value); + + final String value; + + @override + void accept(SourceVisitor visitor) => visitor.visitPattern(value); + + @override + bool get implicit => value.contains('*'); +} + +class _ArtifactSource implements Source { + const _ArtifactSource(this.artifact, { this.platform, this.mode }); + + final Artifact artifact; + final TargetPlatform platform; + final BuildMode mode; + + @override + void accept(SourceVisitor visitor) => visitor.visitArtifact(artifact, platform, mode); + + @override + bool get implicit => false; +} diff --git a/packages/flutter_tools/lib/src/build_system/targets/assets.dart b/packages/flutter_tools/lib/src/build_system/targets/assets.dart new file mode 100644 index 00000000000..e5545db43df --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/assets.dart @@ -0,0 +1,95 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pool/pool.dart'; + +import '../../asset.dart'; +import '../../base/file_system.dart'; +import '../../devfs.dart'; +import '../build_system.dart'; + +/// The copying logic for flutter assets. +// TODO(jonahwilliams): combine the asset bundle logic with this rule so that +// we can compute the key for deleted assets. This is required to remove assets +// from build directories that are no longer part of the manifest and to unify +// the update/diff logic. +class AssetBehavior extends SourceBehavior { + const AssetBehavior(); + + @override + List inputs(Environment environment) { + final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle(); + assetBundle.build( + manifestPath: environment.projectDir.childFile('pubspec.yaml').path, + packagesPath: environment.projectDir.childFile('.packages').path, + ); + final List results = []; + final Iterable files = assetBundle.entries.values.whereType(); + for (DevFSFileContent devFsContent in files) { + results.add(fs.file(devFsContent.file.path)); + } + return results; + } + + @override + List outputs(Environment environment) { + final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle(); + assetBundle.build( + manifestPath: environment.projectDir.childFile('pubspec.yaml').path, + packagesPath: environment.projectDir.childFile('.packages').path, + ); + final List results = []; + for (MapEntry entry in assetBundle.entries.entries) { + final File file = fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', entry.key)); + results.add(file); + } + return results; + } +} + +/// Copies the asset files from the [copyAssets] rule into place. +Future copyAssetsInvocation(Map updates, Environment environment) async { + final Directory output = environment + .buildDir + .childDirectory('flutter_assets'); + if (output.existsSync()) { + output.deleteSync(recursive: true); + } + output.createSync(recursive: true); + final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle(); + await assetBundle.build( + manifestPath: environment.projectDir.childFile('pubspec.yaml').path, + packagesPath: environment.projectDir.childFile('.packages').path, + ); + // Limit number of open files to avoid running out of file descriptors. + final Pool pool = Pool(64); + await Future.wait( + assetBundle.entries.entries.map>((MapEntry entry) async { + final PoolResource resource = await pool.request(); + try { + final File file = fs.file(fs.path.join(output.path, entry.key)); + file.parent.createSync(recursive: true); + await file.writeAsBytes(await entry.value.contentsAsBytes()); + } finally { + resource.release(); + } + })); +} + +/// Copy the assets used in the application into a build directory. +const Target copyAssets = Target( + name: 'copy_assets', + inputs: [ + Source.pattern('{PROJECT_DIR}/pubspec.yaml'), + Source.behavior(AssetBehavior()), + ], + outputs: [ + Source.pattern('{BUILD_DIR}/flutter_assets/AssetManifest.json'), + Source.pattern('{BUILD_DIR}/flutter_assets/FontManifest.json'), + Source.pattern('{BUILD_DIR}/flutter_assets/LICENSE'), + Source.behavior(AssetBehavior()), // <- everything in this subdirectory. + ], + dependencies: [], + buildAction: copyAssetsInvocation, +); diff --git a/packages/flutter_tools/lib/src/build_system/targets/dart.dart b/packages/flutter_tools/lib/src/build_system/targets/dart.dart new file mode 100644 index 00000000000..f7d17f66431 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/dart.dart @@ -0,0 +1,283 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../artifacts.dart'; +import '../../base/build.dart'; +import '../../base/file_system.dart'; +import '../../base/io.dart'; +import '../../base/platform.dart'; +import '../../base/process_manager.dart'; +import '../../build_info.dart'; +import '../../compile.dart'; +import '../../dart/package_map.dart'; +import '../../globals.dart'; +import '../../project.dart'; +import '../build_system.dart'; +import '../exceptions.dart'; + +/// The define to pass a [BuildMode]. +const String kBuildMode= 'BuildMode'; + +/// The define to pass whether we compile 64-bit android-arm code. +const String kTargetPlatform = 'TargetPlatform'; + +/// The define to control what target file is used. +const String kTargetFile = 'TargetFile'; + +/// The define to control what iOS architectures are built for. +/// +/// This is expected to be a comma-separated list of architectures. If not +/// provided, defaults to arm64. +/// +/// The other supported value is armv7, the 32-bit iOS architecture. +const String kIosArchs = 'IosArchs'; + +/// Supports compiling dart source to kernel with a subset of flags. +/// +/// This is a non-incremental compile so the specific [updates] are ignored. +Future compileKernel(Map updates, Environment environment) async { + final KernelCompiler compiler = await kernelCompilerFactory.create( + FlutterProject.fromDirectory(environment.projectDir), + ); + if (environment.defines[kBuildMode] == null) { + throw MissingDefineException(kBuildMode, 'kernel_snapshot'); + } + final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); + final String targetFile = environment.defines[kTargetFile] ?? fs.path.join('lib', 'main.dart'); + + final CompilerOutput output = await compiler.compile( + sdkRoot: artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode), + aot: buildMode != BuildMode.debug, + trackWidgetCreation: false, + targetModel: TargetModel.flutter, + targetProductVm: buildMode == BuildMode.release, + outputFilePath: environment + .buildDir + .childFile('main.app.dill') + .path, + depFilePath: null, + mainPath: targetFile, + ); + if (output.errorCount != 0) { + throw Exception('Errors during snapshot creation: $output'); + } +} + +/// Supports compiling a dart kernel file to an ELF binary. +Future compileAotElf(Map updates, Environment environment) async { + final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false); + final String outputPath = environment.buildDir.path; + if (environment.defines[kBuildMode] == null) { + throw MissingDefineException(kBuildMode, 'aot_elf'); + } + if (environment.defines[kTargetPlatform] == null) { + throw MissingDefineException(kTargetPlatform, 'aot_elf'); + } + final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); + final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]); + final int snapshotExitCode = await snapshotter.build( + platform: targetPlatform, + buildMode: buildMode, + mainPath: environment.buildDir.childFile('main.app.dill').path, + packagesPath: environment.projectDir.childFile('.packages').path, + outputPath: outputPath, + ); + if (snapshotExitCode != 0) { + throw Exception('AOT snapshotter exited with code $snapshotExitCode'); + } +} + +/// Finds the locations of all dart files within the project. +/// +/// This does not attempt to determine if a file is used or imported, so it +/// may otherwise report more files than strictly necessary. +List listDartSources(Environment environment) { + final Map packageMap = PackageMap(environment.projectDir.childFile('.packages').path).map; + final List dartFiles = []; + for (Uri uri in packageMap.values) { + final Directory libDirectory = fs.directory(uri.toFilePath(windows: platform.isWindows)); + for (FileSystemEntity entity in libDirectory.listSync(recursive: true)) { + if (entity is File && entity.path.endsWith('.dart')) { + dartFiles.add(entity); + } + } + } + return dartFiles; +} + +/// Supports compiling a dart kernel file to an assembly file. +/// +/// If more than one iOS arch is provided, then this rule will +/// produce a univeral binary. +Future compileAotAssembly(Map updates, Environment environment) async { + final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false); + final String outputPath = environment.buildDir.path; + if (environment.defines[kBuildMode] == null) { + throw MissingDefineException(kBuildMode, 'aot_assembly'); + } + if (environment.defines[kTargetPlatform] == null) { + throw MissingDefineException(kTargetPlatform, 'aot_assembly'); + } + final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); + final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]); + final List iosArchs = environment.defines[kIosArchs]?.split(',')?.map(getIOSArchForName)?.toList() + ?? [IOSArch.arm64]; + if (targetPlatform != TargetPlatform.ios) { + throw Exception('aot_assembly is only supported for iOS applications'); + } + + // If we're building for a single architecture (common), then skip the lipo. + if (iosArchs.length == 1) { + final int snapshotExitCode = await snapshotter.build( + platform: targetPlatform, + buildMode: buildMode, + mainPath: environment.buildDir.childFile('main.app.dill').path, + packagesPath: environment.projectDir.childFile('.packages').path, + outputPath: outputPath, + iosArch: iosArchs.single, + ); + if (snapshotExitCode != 0) { + throw Exception('AOT snapshotter exited with code $snapshotExitCode'); + } + } else { + // If we're building multiple iOS archs the binaries need to be lipo'd + // together. + final List> pending = >[]; + for (IOSArch iosArch in iosArchs) { + pending.add(snapshotter.build( + platform: targetPlatform, + buildMode: buildMode, + mainPath: environment.buildDir.childFile('main.app.dill').path, + packagesPath: environment.projectDir.childFile('.packages').path, + outputPath: fs.path.join(outputPath, getNameForIOSArch(iosArch)), + iosArch: iosArch, + )); + } + final List results = await Future.wait(pending); + if (results.any((int result) => result != 0)) { + throw Exception('AOT snapshotter exited with code ${results.join()}'); + } + final ProcessResult result = await processManager.run([ + 'lipo', + ...iosArchs.map((IOSArch iosArch) => + fs.path.join(outputPath, getNameForIOSArch(iosArch), 'App.framework', 'App')), + '-create', + '-output', + fs.path.join(outputPath, 'App.framework', 'App'), + ]); + if (result.exitCode != 0) { + throw Exception('lipo exited with code ${result.exitCode}'); + } + } +} + +/// Generate a snapshot of the dart code used in the program. +const Target kernelSnapshot = Target( + name: 'kernel_snapshot', + inputs: [ + Source.function(listDartSources), // <- every dart file under {PROJECT_DIR}/lib and in .packages + Source.artifact(Artifact.platformKernelDill), + Source.artifact(Artifact.engineDartBinary), + Source.artifact(Artifact.frontendServerSnapshotForEngineDartSdk), + ], + outputs: [ + Source.pattern('{BUILD_DIR}/main.app.dill'), + ], + dependencies: [], + buildAction: compileKernel, +); + +/// Generate an ELF binary from a dart kernel file in profile mode. +const Target aotElfProfile = Target( + name: 'aot_elf_profile', + inputs: [ + Source.pattern('{BUILD_DIR}/main.app.dill'), + Source.pattern('{PROJECT_DIR}/.packages'), + Source.artifact(Artifact.engineDartBinary), + Source.artifact(Artifact.skyEnginePath), + Source.artifact(Artifact.genSnapshot, + platform: TargetPlatform.android_arm, + mode: BuildMode.profile, + ), + ], + outputs: [ + Source.pattern('{BUILD_DIR}/app.so'), + ], + dependencies: [ + kernelSnapshot, + ], + buildAction: compileAotElf, +); + +/// Generate an ELF binary from a dart kernel file in release mode. +const Target aotElfRelease= Target( + name: 'aot_elf_release', + inputs: [ + Source.pattern('{BUILD_DIR}/main.app.dill'), + Source.pattern('{PROJECT_DIR}/.packages'), + Source.artifact(Artifact.engineDartBinary), + Source.artifact(Artifact.skyEnginePath), + Source.artifact(Artifact.genSnapshot, + platform: TargetPlatform.android_arm, + mode: BuildMode.release, + ), + ], + outputs: [ + Source.pattern('{BUILD_DIR}/app.so'), + ], + dependencies: [ + kernelSnapshot, + ], + buildAction: compileAotElf, +); + +/// Generate an assembly target from a dart kernel file in profile mode. +const Target aotAssemblyProfile = Target( + name: 'aot_assembly_profile', + inputs: [ + Source.pattern('{BUILD_DIR}/main.app.dill'), + Source.pattern('{PROJECT_DIR}/.packages'), + Source.artifact(Artifact.engineDartBinary), + Source.artifact(Artifact.skyEnginePath), + Source.artifact(Artifact.genSnapshot, + platform: TargetPlatform.ios, + mode: BuildMode.profile, + ), + ], + outputs: [ + // TODO(jonahwilliams): are these used or just a side effect? + // Source.pattern('{BUILD_DIR}/snapshot_assembly.S'), + // Source.pattern('{BUILD_DIR}/snapshot_assembly.o'), + Source.pattern('{BUILD_DIR}/App.framework/App'), + ], + dependencies: [ + kernelSnapshot, + ], + buildAction: compileAotAssembly, +); + +/// Generate an assembly target from a dart kernel file in release mode. +const Target aotAssemblyRelease = Target( + name: 'aot_assembly_release', + inputs: [ + Source.pattern('{BUILD_DIR}/main.app.dill'), + Source.pattern('{PROJECT_DIR}/.packages'), + Source.artifact(Artifact.engineDartBinary), + Source.artifact(Artifact.skyEnginePath), + Source.artifact(Artifact.genSnapshot, + platform: TargetPlatform.ios, + mode: BuildMode.release, + ), + ], + outputs: [ + // TODO(jonahwilliams): are these used or just a side effect? + // Source.pattern('{BUILD_DIR}/snapshot_assembly.S'), + // Source.pattern('{BUILD_DIR}/snapshot_assembly.o'), + Source.pattern('{BUILD_DIR}/App.framework/App'), + ], + dependencies: [ + kernelSnapshot, + ], + buildAction: compileAotAssembly, +); diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart new file mode 100644 index 00000000000..19e38ea484b --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -0,0 +1,43 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../build_system.dart'; +import 'assets.dart'; +import 'dart.dart'; + +/// Create an iOS debug application. +const Target debugIosApplication = Target( + name: 'debug_ios_application', + buildAction: null, + inputs: [], + outputs: [], + dependencies: [ + copyAssets, + kernelSnapshot, + ] +); + +/// Create an iOS profile application. +const Target profileIosApplication = Target( + name: 'profile_ios_application', + buildAction: null, + inputs: [], + outputs: [], + dependencies: [ + copyAssets, + aotAssemblyProfile, + ] +); + +/// Create an iOS debug application. +const Target releaseIosApplication = Target( + name: 'release_ios_application', + buildAction: null, + inputs: [], + outputs: [], + dependencies: [ + copyAssets, + aotAssemblyRelease, + ] +); diff --git a/packages/flutter_tools/lib/src/build_system/targets/linux.dart b/packages/flutter_tools/lib/src/build_system/targets/linux.dart new file mode 100644 index 00000000000..c5ad64d1389 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/linux.dart @@ -0,0 +1,46 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../artifacts.dart'; +import '../../base/file_system.dart'; +import '../../globals.dart'; +import '../build_system.dart'; + +// Copies all of the input files to the correct copy dir. +Future copyLinuxAssets(Map updates, + Environment environment) async { + final String basePath = artifacts.getArtifactPath(Artifact.linuxDesktopPath); + for (String input in updates.keys) { + final String outputPath = fs.path.join( + environment.projectDir.path, + 'linux', + 'flutter', + fs.path.relative(input, from: basePath), + ); + final File destinationFile = fs.file(outputPath); + if (!destinationFile.parent.existsSync()) { + destinationFile.parent.createSync(recursive: true); + } + fs.file(input).copySync(destinationFile.path); + } +} + +/// Copies the Linux desktop embedding files to the copy directory. +const Target unpackLinux = Target( + name: 'unpack_linux', + inputs: [ + Source.artifact(Artifact.linuxDesktopPath), + ], + outputs: [ + Source.pattern('{PROJECT_DIR}/linux/flutter/libflutter_linux.so'), + Source.pattern('{PROJECT_DIR}/linux/flutter/flutter_export.h'), + Source.pattern('{PROJECT_DIR}/linux/flutter/flutter_messenger.h'), + Source.pattern('{PROJECT_DIR}/linux/flutter/flutter_plugin_registrar.h'), + Source.pattern('{PROJECT_DIR}/linux/flutter/flutter_glfw.h'), + Source.pattern('{PROJECT_DIR}/linux/flutter/icudtl.dat'), + Source.pattern('{PROJECT_DIR}/linux/flutter/cpp_client_wrapper/*'), + ], + dependencies: [], + buildAction: copyLinuxAssets, +); diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart new file mode 100644 index 00000000000..7bfb9239f7e --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -0,0 +1,100 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../artifacts.dart'; +import '../../base/file_system.dart'; +import '../../base/io.dart'; +import '../../base/process_manager.dart'; +import '../../globals.dart'; +import '../build_system.dart'; +import 'assets.dart'; +import 'dart.dart'; + +/// Copy the macOS framework to the correct copy dir by invoking 'cp -R'. +/// +/// The shelling out is done to avoid complications with preserving special +/// files (e.g., symbolic links) in the framework structure. +/// +/// Removes any previous version of the framework that already exists in the +/// target directory. +// TODO(jonahwilliams): remove shell out. +Future copyFramework(Map updates, + Environment environment) async { + final String basePath = artifacts.getArtifactPath(Artifact.flutterMacOSFramework); + final Directory targetDirectory = environment + .projectDir + .childDirectory('macos') + .childDirectory('Flutter') + .childDirectory('FlutterMacOS.framework'); + if (targetDirectory.existsSync()) { + targetDirectory.deleteSync(recursive: true); + } + + final ProcessResult result = processManager + .runSync(['cp', '-R', basePath, targetDirectory.path]); + if (result.exitCode != 0) { + throw Exception( + 'Failed to copy framework (exit ${result.exitCode}:\n' + '${result.stdout}\n---\n${result.stderr}', + ); + } +} + +const String _kOutputPrefix = '{PROJECT_DIR}/macos/Flutter/FlutterMacOS.framework'; + +/// Copies the macOS desktop framework to the copy directory. +const Target unpackMacos = Target( + name: 'unpack_macos', + inputs: [ + Source.artifact(Artifact.flutterMacOSFramework), + ], + outputs: [ + Source.pattern('$_kOutputPrefix/FlutterMacOS'), + // Headers + Source.pattern('$_kOutputPrefix/Headers/FLEOpenGLContextHandling.h'), + Source.pattern('$_kOutputPrefix/Headers/FLEReshapeListener.h'), + Source.pattern('$_kOutputPrefix/Headers/FLEView.h'), + Source.pattern('$_kOutputPrefix/Headers/FLEViewController.h'), + Source.pattern('$_kOutputPrefix/Headers/FlutterBinaryMessenger.h'), + Source.pattern('$_kOutputPrefix/Headers/FlutterChannels.h'), + Source.pattern('$_kOutputPrefix/Headers/FlutterCodecs.h'), + Source.pattern('$_kOutputPrefix/Headers/FlutterMacOS.h'), + Source.pattern('$_kOutputPrefix/Headers/FlutterPluginMacOS.h'), + Source.pattern('$_kOutputPrefix/Headers/FlutterPluginRegistrarMacOS.h'), + // Modules + Source.pattern('$_kOutputPrefix/Modules/module.modulemap'), + // Resources + Source.pattern('$_kOutputPrefix/Resources/icudtl.dat'), + Source.pattern('$_kOutputPrefix/Resources/info.plist'), + // Ignore Versions folder for now + ], + dependencies: [], + buildAction: copyFramework, +); + +/// Build a macOS application. +const Target macosApplication = Target( + name: 'debug_macos_application', + buildAction: null, + inputs: [], + outputs: [], + dependencies: [ + unpackMacos, + kernelSnapshot, + copyAssets, + ] +); + +/// Build a macOS release application. +const Target macoReleaseApplication = Target( + name: 'release_macos_application', + buildAction: null, + inputs: [], + outputs: [], + dependencies: [ + unpackMacos, + aotElfRelease, + copyAssets, + ] +); diff --git a/packages/flutter_tools/lib/src/build_system/targets/windows.dart b/packages/flutter_tools/lib/src/build_system/targets/windows.dart new file mode 100644 index 00000000000..2677cba6983 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/windows.dart @@ -0,0 +1,50 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../artifacts.dart'; +import '../../base/file_system.dart'; +import '../../globals.dart'; +import '../build_system.dart'; + +/// Copies all of the input files to the correct copy dir. +Future copyWindowsAssets(Map updates, + Environment environment) async { + // This path needs to match the prefix in the rule below. + final String basePath = artifacts.getArtifactPath(Artifact.windowsDesktopPath); + for (String input in updates.keys) { + final String outputPath = fs.path.join( + environment.projectDir.path, + 'windows', + 'flutter', + fs.path.relative(input, from: basePath), + ); + final File destinationFile = fs.file(outputPath); + if (!destinationFile.parent.existsSync()) { + destinationFile.parent.createSync(recursive: true); + } + fs.file(input).copySync(destinationFile.path); + } +} + +/// Copies the Windows desktop embedding files to the copy directory. +const Target unpackWindows = Target( + name: 'unpack_windows', + inputs: [ + Source.artifact(Artifact.windowsDesktopPath), + ], + outputs: [ + Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_windows.dll'), + Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_windows.dll.exp'), + Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_windows.dll.lib'), + Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_windows.dll.pdb'), + Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_export.h'), + Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_messenger.h'), + Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_plugin_registrar.h'), + Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_glfw.h'), + Source.pattern('{PROJECT_DIR}/windows/flutter/icudtl.dat'), + Source.pattern('{PROJECT_DIR}/windows/flutter/cpp_client_wrapper/*'), + ], + dependencies: [], + buildAction: copyWindowsAssets, +); diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart new file mode 100644 index 00000000000..e5a66593103 --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/assemble.dart @@ -0,0 +1,221 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../base/common.dart'; +import '../base/context.dart'; +import '../base/file_system.dart'; +import '../build_info.dart'; +import '../build_system/build_system.dart'; +import '../convert.dart'; +import '../globals.dart'; +import '../project.dart'; +import '../runner/flutter_command.dart'; + +/// The [BuildSystem] instance. +BuildSystem get buildSystem => context.get(); + +/// Assemble provides a low level API to interact with the flutter tool build +/// system. +class AssembleCommand extends FlutterCommand { + AssembleCommand() { + addSubcommand(AssembleRun()); + addSubcommand(AssembleDescribe()); + addSubcommand(AssembleListInputs()); + addSubcommand(AssembleBuildDirectory()); + } + @override + String get description => 'Assemble and build flutter resources.'; + + @override + String get name => 'assemble'; + + @override + bool get isExperimental => true; + + @override + Future runCommand() { + return null; + } +} + +abstract class AssembleBase extends FlutterCommand { + AssembleBase() { + argParser.addMultiOption( + 'define', + abbr: 'd', + help: 'Allows passing configuration to a target with --define=target=key=value.' + ); + argParser.addOption( + 'build-mode', + allowed: const [ + 'debug', + 'profile', + 'release', + ], + ); + argParser.addOption( + 'resource-pool-size', + help: 'The maximum number of concurrent tasks the build system will run.' + ); + } + + /// Returns the provided target platform. + /// + /// Throws a [ToolExit] if none is provided. This intentionally has no + /// default. + TargetPlatform get targetPlatform { + final String value = argResults['target-platform'] ?? 'darwin-x64'; + if (value == null) { + throwToolExit('--target-platform is required for flutter assemble.'); + } + return getTargetPlatformForName(value); + } + + /// Returns the provided build mode. + /// + /// Throws a [ToolExit] if none is provided. This intentionally has no + /// default. + BuildMode get buildMode { + final String value = argResults['build-mode'] ?? 'debug'; + if (value == null) { + throwToolExit('--build-mode is required for flutter assemble.'); + } + return getBuildModeForName(value); + } + + /// The name of the target we are describing or building. + String get targetName { + if (argResults.rest.isEmpty) { + throwToolExit('missing target name for flutter assemble.'); + } + return argResults.rest.first; + } + + /// The environmental configuration for a build invocation. + Environment get environment { + final FlutterProject flutterProject = FlutterProject.current(); + final Environment result = Environment( + buildDir: fs.directory(getBuildDirectory()), + projectDir: flutterProject.directory, + defines: _parseDefines(argResults['define']), + ); + return result; + } + + static Map _parseDefines(List values) { + final Map results = {}; + for (String chunk in values) { + final List parts = chunk.split('='); + if (parts.length != 2) { + throwToolExit('Improperly formatted define flag: $chunk'); + } + final String key = parts[0]; + final String value = parts[1]; + results[key] = value; + } + return results; + } +} + +/// Execute a build starting from a target action. +class AssembleRun extends AssembleBase { + @override + String get description => 'Execute the stages for a specified target.'; + + @override + String get name => 'run'; + + @override + bool get isExperimental => true; + + @override + Future runCommand() async { + final BuildResult result = await buildSystem.build(targetName, environment, BuildSystemConfig( + resourcePoolSize: argResults['resource-pool-size'], + )); + if (!result.success) { + for (MapEntry data in result.exceptions.entries) { + printError('Target ${data.key} failed: ${data.value.exception}'); + printError('${data.value.exception}'); + } + throwToolExit('build failed'); + } else { + printStatus('build succeeded'); + } + return null; + } +} + +/// Fully describe a target and its dependencies. +class AssembleDescribe extends AssembleBase { + @override + String get description => 'List the stages for a specified target.'; + + @override + String get name => 'describe'; + + @override + bool get isExperimental => true; + + @override + Future runCommand() { + try { + printStatus( + json.encode(buildSystem.describe(targetName, environment)) + ); + } on Exception catch (err, stackTrace) { + printTrace(stackTrace.toString()); + throwToolExit(err.toString()); + } + return null; + } +} + +/// List input files for a target. +class AssembleListInputs extends AssembleBase { + @override + String get description => 'List the inputs for a particular target.'; + + @override + String get name => 'inputs'; + + @override + bool get isExperimental => true; + + @override + Future runCommand() { + try { + final List> results = buildSystem.describe(targetName, environment); + for (Map result in results) { + if (result['name'] == targetName) { + final List inputs = result['inputs']; + inputs.forEach(printStatus); + } + } + } on Exception catch (err, stackTrace) { + printTrace(stackTrace.toString()); + throwToolExit(err.toString()); + } + return null; + } +} + +/// Return the build directory for a configuiration. +class AssembleBuildDirectory extends AssembleBase { + @override + String get description => 'List the inputs for a particular target.'; + + @override + String get name => 'build-dir'; + + @override + bool get isExperimental => true; + + @override + Future runCommand() { + printStatus(environment.buildDir.path); + return null; + } +} + diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index e06d1c938c0..b7a477d695c 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -277,7 +277,7 @@ class AttachCommand extends FlutterCommand { target: targetFile, debuggingOptions: debuggingOptions, packagesFilePath: globalResults['packages'], - usesTerminalUI: daemon == null, + usesTerminalUi: daemon == null, projectRootPath: argResults['project-root'], dillOutputPath: argResults['output-dill'], ipv6: usesIpv6, @@ -312,7 +312,15 @@ class AttachCommand extends FlutterCommand { result = await app.runner.waitForAppToFinish(); assert(result != null); } else { - result = await runner.attach(); + final Completer onAppStart = Completer.sync(); + unawaited(onAppStart.future.whenComplete(() { + TerminalHandler(runner) + ..setupTerminal() + ..registerSignalHandlers(); + })); + result = await runner.attach( + appStartedCompleter: onAppStart, + ); assert(result != null); } if (result != 0) { @@ -350,7 +358,7 @@ class HotRunnerFactory { List devices, { String target, DebuggingOptions debuggingOptions, - bool usesTerminalUI = true, + bool usesTerminalUi = true, bool benchmarkMode = false, File applicationBinary, bool hostIsIde = false, @@ -364,7 +372,7 @@ class HotRunnerFactory { devices, target: target, debuggingOptions: debuggingOptions, - usesTerminalUI: usesTerminalUI, + usesTerminalUi: usesTerminalUi, benchmarkMode: benchmarkMode, applicationBinary: applicationBinary, hostIsIde: hostIsIde, diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index dff4b34e731..a441ecf8786 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -419,7 +419,7 @@ class AppDomain extends Domain { [flutterDevice], target: target, debuggingOptions: options, - usesTerminalUI: false, + usesTerminalUi: false, applicationBinary: applicationBinary, projectRootPath: projectRootPath, packagesFilePath: packagesFilePath, @@ -432,8 +432,8 @@ class AppDomain extends Domain { [flutterDevice], target: target, debuggingOptions: options, - usesTerminalUI: false, applicationBinary: applicationBinary, + usesTerminalUi: false, ipv6: ipv6, ); } diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index aeabe1f617d..35251f2e805 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -450,8 +450,8 @@ class RunCommand extends RunCommandBase { applicationBinary: applicationBinaryPath == null ? null : fs.file(applicationBinaryPath), - stayResident: stayResident, ipv6: ipv6, + stayResident: stayResident, ); } @@ -463,7 +463,14 @@ class RunCommand extends RunCommandBase { final Completer appStartedTimeRecorder = Completer.sync(); // This callback can't throw. unawaited(appStartedTimeRecorder.future.then( - (_) { appStartedTime = systemClock.now(); } + (_) { + appStartedTime = systemClock.now(); + if (stayResident) { + TerminalHandler(runner) + ..setupTerminal() + ..registerSignalHandlers(); + } + } )); final int result = await runner.run( @@ -471,8 +478,9 @@ class RunCommand extends RunCommandBase { route: route, shouldBuild: !runningWithPrebuiltApplication && argResults['build'], ); - if (result != 0) + if (result != 0) { throwToolExit(null, exitCode: result); + } return FlutterCommandResult( ExitStatus.success, timingLabelParts: [ diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index bc03cbe4474..533cfc6fbdf 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -21,6 +21,7 @@ import 'base/platform.dart'; import 'base/time.dart'; import 'base/user_messages.dart'; import 'base/utils.dart'; +import 'build_system/build_system.dart'; import 'cache.dart'; import 'compile.dart'; import 'devfs.dart'; @@ -67,6 +68,7 @@ Future runInContext( Artifacts: () => CachedArtifacts(), AssetBundleFactory: () => AssetBundleFactory.defaultInstance, BotDetector: () => const BotDetector(), + BuildSystem: () => BuildSystem(), Cache: () => Cache(), ChromeLauncher: () => const ChromeLauncher(), CocoaPods: () => CocoaPods(), diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 1bda14808bb..5865744ae7f 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -406,8 +406,8 @@ abstract class Device { DebuggingOptions debuggingOptions, Map platformArgs, bool prebuiltApplication = false, - bool usesTerminalUi = true, bool ipv6 = false, + bool usesTerminalUi = true, }); /// Whether this device implements support for hot reload. diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 2cb910947d3..77c9e94e52b 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -115,7 +115,7 @@ class FlutterDevice { /// expressions requested during debugging of the application. /// This ensures that the reload process follows the normal orchestration of /// the Flutter Tools and not just the VM internal service. - Future _connect({ + Future connect({ ReloadSources reloadSources, Restart restart, CompileExpression compileExpression, @@ -375,7 +375,7 @@ class FlutterDevice { platformArgs: platformArgs, route: route, prebuiltApplication: prebuiltMode, - usesTerminalUi: hotRunner.usesTerminalUI, + usesTerminalUi: hotRunner.usesTerminalUi, ipv6: hotRunner.ipv6, ); @@ -437,7 +437,7 @@ class FlutterDevice { platformArgs: platformArgs, route: route, prebuiltApplication: prebuiltMode, - usesTerminalUi: coldRunner.usesTerminalUI, + usesTerminalUi: coldRunner.usesTerminalUi, ipv6: coldRunner.ipv6, ); @@ -509,11 +509,12 @@ abstract class ResidentRunner { this.flutterDevices, { this.target, this.debuggingOptions, - this.usesTerminalUI = true, String projectRootPath, String packagesFilePath, - this.stayResident, this.ipv6, + this.usesTerminalUi = true, + this.stayResident = true, + this.hotMode = true, }) { _mainPath = findMainDartFile(target); _projectRootPath = projectRootPath ?? fs.currentDirectory.path; @@ -525,11 +526,12 @@ abstract class ResidentRunner { final List flutterDevices; final String target; final DebuggingOptions debuggingOptions; - final bool usesTerminalUI; + final bool usesTerminalUi; final bool stayResident; final bool ipv6; final Completer _finished = Completer(); bool _exited = false; + bool hotMode ; String _packagesFilePath; String get packagesFilePath => _packagesFilePath; String _projectRootPath; @@ -557,6 +559,9 @@ abstract class ResidentRunner { }); } + /// Whether this runner can hot reload. + bool get canHotReload => hotMode; + /// Start the app and keep the process running during its lifetime. /// /// Returns the exit code that we should use for the flutter tool process; 0 @@ -601,68 +606,68 @@ abstract class ResidentRunner { await Future.wait(futures); } - Future _debugDumpApp() async { + Future debugDumpApp() async { await refreshViews(); for (FlutterDevice device in flutterDevices) await device.debugDumpApp(); } - Future _debugDumpRenderTree() async { + Future debugDumpRenderTree() async { await refreshViews(); for (FlutterDevice device in flutterDevices) await device.debugDumpRenderTree(); } - Future _debugDumpLayerTree() async { + Future debugDumpLayerTree() async { await refreshViews(); for (FlutterDevice device in flutterDevices) await device.debugDumpLayerTree(); } - Future _debugDumpSemanticsTreeInTraversalOrder() async { + Future debugDumpSemanticsTreeInTraversalOrder() async { await refreshViews(); for (FlutterDevice device in flutterDevices) await device.debugDumpSemanticsTreeInTraversalOrder(); } - Future _debugDumpSemanticsTreeInInverseHitTestOrder() async { + Future debugDumpSemanticsTreeInInverseHitTestOrder() async { await refreshViews(); for (FlutterDevice device in flutterDevices) await device.debugDumpSemanticsTreeInInverseHitTestOrder(); } - Future _debugToggleDebugPaintSizeEnabled() async { + Future debugToggleDebugPaintSizeEnabled() async { await refreshViews(); for (FlutterDevice device in flutterDevices) await device.toggleDebugPaintSizeEnabled(); } - Future _debugToggleDebugCheckElevationsEnabled() async { + Future debugToggleDebugCheckElevationsEnabled() async { await refreshViews(); for (FlutterDevice device in flutterDevices) await device.toggleDebugCheckElevationsEnabled(); } - Future _debugTogglePerformanceOverlayOverride() async { + Future debugTogglePerformanceOverlayOverride() async { await refreshViews(); for (FlutterDevice device in flutterDevices) await device.debugTogglePerformanceOverlayOverride(); } - Future _debugToggleWidgetInspector() async { + Future debugToggleWidgetInspector() async { await refreshViews(); for (FlutterDevice device in flutterDevices) await device.toggleWidgetInspector(); } - Future _debugToggleProfileWidgetBuilds() async { + Future debugToggleProfileWidgetBuilds() async { await refreshViews(); for (FlutterDevice device in flutterDevices) { await device.toggleProfileWidgetBuilds(); } } - Future _screenshot(FlutterDevice device) async { + Future screenshot(FlutterDevice device) async { final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...', timeout: timeoutConfiguration.fastOperation); final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png'); try { @@ -700,7 +705,7 @@ abstract class ResidentRunner { } } - Future _debugTogglePlatform() async { + Future debugTogglePlatform() async { await refreshViews(); final String from = await flutterDevices[0].views[0].uiIsolate.flutterPlatformOverride(); String to; @@ -709,39 +714,6 @@ abstract class ResidentRunner { printStatus('Switched operating system to $to'); } - void registerSignalHandlers() { - assert(stayResident); - io.ProcessSignal.SIGINT.watch().listen(_cleanUpAndExit); - io.ProcessSignal.SIGTERM.watch().listen(_cleanUpAndExit); - if (!supportsServiceProtocol || !supportsRestart) - return; - io.ProcessSignal.SIGUSR1.watch().listen(_handleSignal); - io.ProcessSignal.SIGUSR2.watch().listen(_handleSignal); - } - - Future _cleanUpAndExit(io.ProcessSignal signal) async { - _resetTerminal(); - await cleanupAfterSignal(); - io.exit(0); - } - - bool _processingUserRequest = false; - Future _handleSignal(io.ProcessSignal signal) async { - if (_processingUserRequest) { - printTrace('Ignoring signal: "$signal" because we are busy.'); - return; - } - _processingUserRequest = true; - - final bool fullRestart = signal == io.ProcessSignal.SIGUSR2; - - try { - await restart(fullRestart: fullRestart); - } finally { - _processingUserRequest = false; - } - } - Future stopEchoingDeviceLog() async { await Future.wait( flutterDevices.map>((FlutterDevice device) => device.stopEchoingDeviceLog()) @@ -764,7 +736,7 @@ abstract class ResidentRunner { bool viewFound = false; for (FlutterDevice device in flutterDevices) { - await device._connect( + await device.connect( reloadSources: reloadSources, restart: restart, compileExpression: compileExpression, @@ -805,125 +777,6 @@ abstract class ResidentRunner { return Future.error(error, stack); } - /// Returns [true] if the input has been handled by this function. - Future _commonTerminalInputHandler(String character) async { - - printStatus(''); // the key the user tapped might be on this line - switch(character) { - case 'a': - if (supportsServiceProtocol) { - await _debugToggleProfileWidgetBuilds(); - return true; - } - return false; - case 'd': - case 'D': - await detach(); - return true; - case 'h': - case 'H': - case '?': - // help - printHelp(details: true); - return true; - case 'i': - case 'I': - if (supportsServiceProtocol) { - await _debugToggleWidgetInspector(); - return true; - } - return false; - case 'L': - if (supportsServiceProtocol) { - await _debugDumpLayerTree(); - return true; - } - return false; - case 'o': - case 'O': - if (supportsServiceProtocol && isRunningDebug) { - await _debugTogglePlatform(); - return true; - } - return false; - case 'p': - if (supportsServiceProtocol && isRunningDebug) { - await _debugToggleDebugPaintSizeEnabled(); - return true; - } - return false; - case 'P': - if (supportsServiceProtocol) { - await _debugTogglePerformanceOverlayOverride(); - return true; - } - return false; - case 'q': - case 'Q': - // exit - await exit(); - return true; - case 's': - for (FlutterDevice device in flutterDevices) { - if (device.device.supportsScreenshot) - await _screenshot(device); - } - return true; - case 'S': - if (supportsServiceProtocol) { - await _debugDumpSemanticsTreeInTraversalOrder(); - return true; - } - return false; - case 't': - case 'T': - if (supportsServiceProtocol) { - await _debugDumpRenderTree(); - return true; - } - return false; - case 'U': - if (supportsServiceProtocol) { - await _debugDumpSemanticsTreeInInverseHitTestOrder(); - return true; - } - return false; - case 'w': - case 'W': - if (supportsServiceProtocol) { - await _debugDumpApp(); - return true; - } - return false; - case 'z': - case 'Z': - await _debugToggleDebugCheckElevationsEnabled(); - return true; - } - - return false; - } - - Future processTerminalInput(String command) async { - // When terminal doesn't support line mode, '\n' can sneak into the input. - command = command.trim(); - if (_processingUserRequest) { - printTrace('Ignoring terminal input: "$command" because we are busy.'); - return; - } - _processingUserRequest = true; - try { - final bool handled = await _commonTerminalInputHandler(command); - if (!handled) - await handleTerminalCommand(command); - } catch (error, st) { - printError('$error\n$st'); - await _cleanUpAndExit(null); - } finally { - _processingUserRequest = false; - } - } - void _serviceDisconnected() { if (_exited) { // User requested the application exit. @@ -932,7 +785,6 @@ abstract class ResidentRunner { if (_finished.isCompleted) return; printStatus('Lost connection to device.'); - _resetTerminal(); _finished.complete(0); } @@ -940,27 +792,9 @@ abstract class ResidentRunner { if (_finished.isCompleted) return; printStatus('Application finished.'); - _resetTerminal(); _finished.complete(0); } - void _resetTerminal() { - if (usesTerminalUI) - terminal.singleCharMode = false; - } - - void setupTerminal() { - assert(stayResident); - if (usesTerminalUI) { - if (!logger.quiet) { - printStatus(''); - printHelp(details: false); - } - terminal.singleCharMode = true; - terminal.keystrokes.listen(processTerminalInput); - } - } - Future waitForAppToFinish() async { final int exitCode = await _finished.future; assert(exitCode != null); @@ -1004,10 +838,9 @@ abstract class ResidentRunner { /// Called when a signal has requested we exit. Future cleanupAfterSignal(); + /// Called right before we exit. Future cleanupAtFinish(); - /// Called when the runner should handle a terminal command. - Future handleTerminalCommand(String code); } class OperationResult { @@ -1051,6 +884,203 @@ Future getMissingPackageHintForPlatform(TargetPlatform platform) async { } } +/// Redirects terminal commands to the correct resident runner methods. +class TerminalHandler { + TerminalHandler(this.residentRunner); + + final ResidentRunner residentRunner; + bool _processingUserRequest = false; + StreamSubscription subscription; + + @visibleForTesting + String lastReceivedCommand; + + void setupTerminal() { + if (!logger.quiet) { + printStatus(''); + residentRunner.printHelp(details: false); + } + terminal.singleCharMode = true; + subscription = terminal.keystrokes.listen(processTerminalInput); + } + + void registerSignalHandlers() { + assert(residentRunner.stayResident); + io.ProcessSignal.SIGINT.watch().listen(_cleanUpAndExit); + io.ProcessSignal.SIGTERM.watch().listen(_cleanUpAndExit); + if (!residentRunner.supportsServiceProtocol || !residentRunner.supportsRestart) + return; + io.ProcessSignal.SIGUSR1.watch().listen(_handleSignal); + io.ProcessSignal.SIGUSR2.watch().listen(_handleSignal); + } + + /// Returns [true] if the input has been handled by this function. + Future _commonTerminalInputHandler(String character) async { + printStatus(''); // the key the user tapped might be on this line + switch(character) { + case 'a': + if (residentRunner.supportsServiceProtocol) { + await residentRunner.debugToggleProfileWidgetBuilds(); + return true; + } + return false; + case 'd': + case 'D': + await residentRunner.detach(); + return true; + case 'h': + case 'H': + case '?': + // help + residentRunner.printHelp(details: true); + return true; + case 'i': + case 'I': + if (residentRunner.supportsServiceProtocol) { + await residentRunner.debugToggleWidgetInspector(); + return true; + } + return false; + case 'l': + final List views = residentRunner.flutterDevices + .expand((FlutterDevice d) => d.views).toList(); + printStatus('Connected ${pluralize('view', views.length)}:'); + for (FlutterView v in views) { + printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2); + } + return true; + case 'L': + if (residentRunner.supportsServiceProtocol) { + await residentRunner.debugDumpLayerTree(); + return true; + } + return false; + case 'o': + case 'O': + if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) { + await residentRunner.debugTogglePlatform(); + return true; + } + return false; + case 'p': + if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) { + await residentRunner.debugToggleDebugPaintSizeEnabled(); + return true; + } + return false; + case 'P': + if (residentRunner.supportsServiceProtocol) { + await residentRunner.debugTogglePerformanceOverlayOverride(); + return true; + } + return false; + case 'q': + case 'Q': + // exit + await residentRunner.exit(); + return true; + case 's': + for (FlutterDevice device in residentRunner.flutterDevices) { + if (device.device.supportsScreenshot) + await residentRunner.screenshot(device); + } + return true; + case 'r': + if (!residentRunner.canHotReload) { + return false; + } + final OperationResult result = await residentRunner.restart(fullRestart: false); + if (!result.isOk) { + printStatus('Try again after fixing the above error(s).', emphasis: true); + } + return true; + case 'R': + // If hot restart is not supported for all devices, ignore the command. + if (!residentRunner.canHotRestart || !residentRunner.hotMode) { + return false; + } + final OperationResult result = await residentRunner.restart(fullRestart: true); + if (!result.isOk) { + printStatus('Try again after fixing the above error(s).', emphasis: true); + } + return true; + case 'S': + if (residentRunner.supportsServiceProtocol) { + await residentRunner.debugDumpSemanticsTreeInTraversalOrder(); + return true; + } + return false; + case 't': + case 'T': + if (residentRunner.supportsServiceProtocol) { + await residentRunner.debugDumpRenderTree(); + return true; + } + return false; + case 'U': + if (residentRunner.supportsServiceProtocol) { + await residentRunner.debugDumpSemanticsTreeInInverseHitTestOrder(); + return true; + } + return false; + case 'w': + case 'W': + if (residentRunner.supportsServiceProtocol) { + await residentRunner.debugDumpApp(); + return true; + } + return false; + case 'z': + case 'Z': + await residentRunner.debugToggleDebugCheckElevationsEnabled(); + return true; + } + return false; + } + + Future processTerminalInput(String command) async { + // When terminal doesn't support line mode, '\n' can sneak into the input. + command = command.trim(); + if (_processingUserRequest) { + printTrace('Ignoring terminal input: "$command" because we are busy.'); + return; + } + _processingUserRequest = true; + try { + lastReceivedCommand = command; + await _commonTerminalInputHandler(command); + } catch (error, st) { + printError('$error\n$st'); + await _cleanUpAndExit(null); + } finally { + _processingUserRequest = false; + } + } + + Future _handleSignal(io.ProcessSignal signal) async { + if (_processingUserRequest) { + printTrace('Ignoring signal: "$signal" because we are busy.'); + return; + } + _processingUserRequest = true; + + final bool fullRestart = signal == io.ProcessSignal.SIGUSR2; + + try { + await residentRunner.restart(fullRestart: fullRestart); + } finally { + _processingUserRequest = false; + } + } + + Future _cleanUpAndExit(io.ProcessSignal signal) async { + terminal.singleCharMode = false; + await subscription.cancel(); + await residentRunner.cleanupAfterSignal(); + io.exit(0); + } +} + class DebugConnectionInfo { DebugConnectionInfo({ this.httpUri, this.wsUri, this.baseUri }); diff --git a/packages/flutter_tools/lib/src/resident_web_runner.dart b/packages/flutter_tools/lib/src/resident_web_runner.dart index 36c3d1243b2..be3753639d2 100644 --- a/packages/flutter_tools/lib/src/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/resident_web_runner.dart @@ -37,10 +37,10 @@ class ResidentWebRunner extends ResidentRunner { }) : super( flutterDevices, target: target, - usesTerminalUI: true, - stayResident: true, debuggingOptions: debuggingOptions, ipv6: ipv6, + usesTerminalUi: true, + stayResident: true, ); WebAssetServer _server; @@ -49,12 +49,14 @@ class ResidentWebRunner extends ResidentRunner { WipConnection _connection; final FlutterProject flutterProject; + @override + bool get canHotReload => false; + @override Future attach( {Completer connectionInfoCompleter, Completer appStartedCompleter}) async { connectionInfoCompleter?.complete(DebugConnectionInfo()); - setupTerminal(); final int result = await waitForAppToFinish(); await cleanupAtFinish(); return result; @@ -74,17 +76,6 @@ class ResidentWebRunner extends ResidentRunner { await _server?.dispose(); } - @override - Future handleTerminalCommand(String code) async { - if (code == 'R') { - // If hot restart is not supported for all devices, ignore the command. - if (!canHotRestart) { - return; - } - await restart(fullRestart: true); - } - } - @override void printHelp({bool details}) { const String fire = '🔥'; diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart index b2f721447fc..6a79afc46ff 100644 --- a/packages/flutter_tools/lib/src/run_cold.dart +++ b/packages/flutter_tools/lib/src/run_cold.dart @@ -19,16 +19,17 @@ class ColdRunner extends ResidentRunner { List devices, { String target, DebuggingOptions debuggingOptions, - bool usesTerminalUI = true, this.traceStartup = false, this.awaitFirstFrameWhenTracing = true, this.applicationBinary, - bool stayResident = true, bool ipv6 = false, + bool usesTerminalUi = false, + bool stayResident = true, }) : super(devices, target: target, debuggingOptions: debuggingOptions, - usesTerminalUI: usesTerminalUI, + hotMode: false, + usesTerminalUi: usesTerminalUi, stayResident: stayResident, ipv6: ipv6); @@ -37,6 +38,12 @@ class ColdRunner extends ResidentRunner { final File applicationBinary; bool _didAttach = false; + @override + bool get canHotReload => false; + + @override + bool get canHotRestart => false; + @override Future run({ Completer connectionInfoCompleter, @@ -104,9 +111,6 @@ class ColdRunner extends ResidentRunner { ); } appFinished(); - } else if (stayResident) { - setupTerminal(); - registerSignalHandlers(); } appStartedCompleter?.complete(); @@ -138,10 +142,6 @@ class ColdRunner extends ResidentRunner { printTrace('Connected to $view.'); } } - if (stayResident) { - setupTerminal(); - registerSignalHandlers(); - } appStartedCompleter?.complete(); if (stayResident) { return waitForAppToFinish(); @@ -150,9 +150,6 @@ class ColdRunner extends ResidentRunner { return 0; } - @override - Future handleTerminalCommand(String code) async { } - @override Future cleanupAfterSignal() async { await stopEchoingDeviceLog(); diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 767b4b5aaa1..c19057ecf9a 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -57,7 +57,7 @@ class HotRunner extends ResidentRunner { List devices, { String target, DebuggingOptions debuggingOptions, - bool usesTerminalUI = true, + bool usesTerminalUi = true, this.benchmarkMode = false, this.applicationBinary, this.hostIsIde = false, @@ -69,10 +69,11 @@ class HotRunner extends ResidentRunner { }) : super(devices, target: target, debuggingOptions: debuggingOptions, - usesTerminalUI: usesTerminalUI, + usesTerminalUi: usesTerminalUi, projectRootPath: projectRootPath, packagesFilePath: packagesFilePath, stayResident: stayResident, + hotMode: true, ipv6: ipv6); final bool benchmarkMode; @@ -194,11 +195,6 @@ class HotRunner extends ResidentRunner { printTrace('Connected to $view.'); } - if (stayResident) { - setupTerminal(); - registerSignalHandlers(); - } - appStartedCompleter?.complete(); if (benchmarkMode) { @@ -264,32 +260,6 @@ class HotRunner extends ResidentRunner { ); } - @override - Future handleTerminalCommand(String code) async { - final String lower = code.toLowerCase(); - if (lower == 'r') { - OperationResult result; - if (code == 'R') { - // If hot restart is not supported for all devices, ignore the command. - if (!canHotRestart) { - return; - } - result = await restart(fullRestart: true); - } else { - result = await restart(fullRestart: false); - } - if (!result.isOk) { - printStatus('Try again after fixing the above error(s).', emphasis: true); - } - } else if (lower == 'l') { - final List views = flutterDevices.expand((FlutterDevice d) => d.views).toList(); - printStatus('Connected ${pluralize('view', views.length)}:'); - for (FlutterView v in views) { - printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2); - } - } - } - Future> _initDevFS() async { final String fsName = fs.path.basename(projectRootPath); final List devFSUris = []; diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 31dd3697096..b05aa2ddd06 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -140,7 +140,7 @@ class VMService { // If the Flutter Engine doesn't support service registration this will // have no effect - _peer.sendNotification('_registerService', { + _peer.sendNotification('registerService', { 'service': 'reloadSources', 'alias': 'Flutter Tools', }); @@ -166,7 +166,7 @@ class VMService { // If the Flutter Engine doesn't support service registration this will // have no effect - _peer.sendNotification('_registerService', { + _peer.sendNotification('registerService', { 'service': 'hotRestart', 'alias': 'Flutter Tools', }); @@ -204,7 +204,7 @@ class VMService { } }); - _peer.sendNotification('_registerService', { + _peer.sendNotification('registerService', { 'service': 'compileExpression', 'alias': 'Flutter Tools', }); diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 533718d790e..6a573d1bd8b 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: args: 1.5.2 bsdiff: 0.1.0 completion: 0.2.1+1 - coverage: 0.12.4 + coverage: 0.13.0 crypto: 2.0.6 file: 5.0.8+1 http: 0.12.0+2 @@ -32,12 +32,13 @@ dependencies: stream_channel: 2.0.0 usage: 3.4.1 vm_service_client: 0.2.6+2 - web_socket_channel: 1.0.13 + web_socket_channel: 1.0.14 webkit_inspection_protocol: 0.4.2 xml: 3.5.0 yaml: 2.1.16 flutter_goldens_client: path: ../flutter_goldens_client + protobuf: 0.13.15 # We depend on very specific internal implementation details of the # 'test' package, which change between versions, so when upgrading @@ -47,7 +48,7 @@ dependencies: # Code generation dependencies build_runner_core: 3.0.6 - dart_style: 1.2.8 + dart_style: 1.2.9 code_builder: 3.2.0 build: 1.1.4 build_modules: 2.3.0 @@ -61,10 +62,10 @@ dependencies: build_config: 0.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" build_resolvers: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" built_collection: 4.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - built_value: 6.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + built_value: 6.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" charcode: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - csslib: 0.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" fixnum: 0.10.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" front_end: 0.1.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -83,7 +84,6 @@ dependencies: pedantic: 1.8.0+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" petitparser: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pool: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - protobuf: 0.13.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pub_semver: 1.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pubspec_parse: 0.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" scratch_space: 0.0.3+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -103,7 +103,7 @@ dev_dependencies: collection: 1.14.11 mockito: 4.1.0 file_testing: 2.1.0 - vm_service_lib: 3.21.0 + vm_service_lib: 3.22.0 test: 1.6.3 build_runner: 1.6.1 build_vm_compilers: 1.0.0 @@ -119,4 +119,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: f11f +# PUBSPEC CHECKSUM: 7d24 diff --git a/packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 3d43d11e66f..dc9ada4725e 100644 Binary files a/packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 3d43d11e66f..dc9ada4725e 100644 Binary files a/packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/flutter_tools/templates/module/ios/host_app_ephemeral_cocoapods/Podfile.copy.tmpl b/packages/flutter_tools/templates/module/ios/host_app_ephemeral_cocoapods/Podfile.copy.tmpl index 3457cfd81b3..b2b9a405b6d 100644 --- a/packages/flutter_tools/templates/module/ios/host_app_ephemeral_cocoapods/Podfile.copy.tmpl +++ b/packages/flutter_tools/templates/module/ios/host_app_ephemeral_cocoapods/Podfile.copy.tmpl @@ -4,3 +4,6 @@ target 'Runner' do flutter_application_path = '../' eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding) end + +# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. +install! 'cocoapods', :disable_input_output_paths => true diff --git a/packages/flutter_tools/test/build_system/build_system_test.dart b/packages/flutter_tools/test/build_system/build_system_test.dart new file mode 100644 index 00000000000..e0c2ffa7ab6 --- /dev/null +++ b/packages/flutter_tools/test/build_system/build_system_test.dart @@ -0,0 +1,554 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/build_system/exceptions.dart'; +import 'package:flutter_tools/src/build_system/file_hash_store.dart'; +import 'package:flutter_tools/src/build_system/filecache.pb.dart' as pb; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/convert.dart'; +import 'package:mockito/mockito.dart'; + +import '../src/common.dart'; +import '../src/context.dart'; +import '../src/testbed.dart'; + +void main() { + setUpAll(() { + Cache.disableLocking(); + }); + + group(Target, () { + Testbed testbed; + MockPlatform mockPlatform; + Environment environment; + Target fooTarget; + Target barTarget; + Target fizzTarget; + BuildSystem buildSystem; + int fooInvocations; + int barInvocations; + + setUp(() { + fooInvocations = 0; + barInvocations = 0; + mockPlatform = MockPlatform(); + // Keep file paths the same. + when(mockPlatform.isWindows).thenReturn(false); + testbed = Testbed( + setup: () { + environment = Environment( + projectDir: fs.currentDirectory, + ); + fs.file('foo.dart').createSync(recursive: true); + fs.file('pubspec.yaml').createSync(); + fooTarget = Target( + name: 'foo', + inputs: const [ + Source.pattern('{PROJECT_DIR}/foo.dart'), + ], + outputs: const [ + Source.pattern('{BUILD_DIR}/out'), + ], + dependencies: [], + buildAction: (Map updates, Environment environment) { + environment + .buildDir + .childFile('out') + ..createSync(recursive: true) + ..writeAsStringSync('hey'); + fooInvocations++; + } + ); + barTarget = Target( + name: 'bar', + inputs: const [ + Source.pattern('{BUILD_DIR}/out'), + ], + outputs: const [ + Source.pattern('{BUILD_DIR}/bar'), + ], + dependencies: [fooTarget], + buildAction: (Map updates, Environment environment) { + environment.buildDir + .childFile('bar') + ..createSync(recursive: true) + ..writeAsStringSync('there'); + barInvocations++; + } + ); + fizzTarget = Target( + name: 'fizz', + inputs: const [ + Source.pattern('{BUILD_DIR}/out'), + ], + outputs: const [ + Source.pattern('{BUILD_DIR}/fizz'), + ], + dependencies: [fooTarget], + buildAction: (Map updates, Environment environment) { + throw Exception('something bad happens'); + } + ); + buildSystem = BuildSystem({ + fooTarget.name: fooTarget, + barTarget.name: barTarget, + fizzTarget.name: fizzTarget, + }); + }, + overrides: { + Platform: () => mockPlatform, + } + ); + }); + + test('can describe build rules', () => testbed.run(() { + expect(buildSystem.describe('foo', environment), [ + { + 'name': 'foo', + 'dependencies': [], + 'inputs': ['/foo.dart'], + 'outputs': [fs.path.join(environment.buildDir.path, 'out')], + 'stamp': fs.path.join(environment.buildDir.path, 'foo.stamp'), + } + ]); + })); + + test('Throws exception if asked to build non-existent target', () => testbed.run(() { + expect(buildSystem.build('not_real', environment, const BuildSystemConfig()), throwsA(isInstanceOf())); + })); + + test('Throws exception if asked to build with missing inputs', () => testbed.run(() async { + // Delete required input file. + fs.file('foo.dart').deleteSync(); + final BuildResult buildResult = await buildSystem.build('foo', environment, const BuildSystemConfig()); + + expect(buildResult.hasException, true); + expect(buildResult.exceptions.values.single.exception, isInstanceOf()); + })); + + test('Throws exception if it does not produce a specified output', () => testbed.run(() async { + final Target badTarget = Target + (buildAction: (Map inputs, Environment environment) {}, + inputs: const [ + Source.pattern('{PROJECT_DIR}/foo.dart'), + ], + outputs: const [ + Source.pattern('{BUILD_DIR}/out') + ], + name: 'bad' + ); + buildSystem = BuildSystem({ + badTarget.name: badTarget, + }); + final BuildResult result = await buildSystem.build('bad', environment, const BuildSystemConfig()); + + expect(result.hasException, true); + expect(result.exceptions.values.single.exception, isInstanceOf()); + })); + + test('Saves a stamp file with inputs and outputs', () => testbed.run(() async { + await buildSystem.build('foo', environment, const BuildSystemConfig()); + + final File stampFile = fs.file(fs.path.join(environment.buildDir.path, 'foo.stamp')); + expect(stampFile.existsSync(), true); + + final Map stampContents = json.decode(stampFile.readAsStringSync()); + expect(stampContents['inputs'], ['/foo.dart']); + })); + + test('Does not re-invoke build if stamp is valid', () => testbed.run(() async { + await buildSystem.build('foo', environment, const BuildSystemConfig()); + await buildSystem.build('foo', environment, const BuildSystemConfig()); + + expect(fooInvocations, 1); + })); + + test('Re-invoke build if input is modified', () => testbed.run(() async { + await buildSystem.build('foo', environment, const BuildSystemConfig()); + + fs.file('foo.dart').writeAsStringSync('new contents'); + + await buildSystem.build('foo', environment, const BuildSystemConfig()); + expect(fooInvocations, 2); + })); + + test('does not re-invoke build if input timestamp changes', () => testbed.run(() async { + await buildSystem.build('foo', environment, const BuildSystemConfig()); + + fs.file('foo.dart').writeAsStringSync(''); + + await buildSystem.build('foo', environment, const BuildSystemConfig()); + expect(fooInvocations, 1); + })); + + test('does not re-invoke build if output timestamp changes', () => testbed.run(() async { + await buildSystem.build('foo', environment, const BuildSystemConfig()); + + environment.buildDir.childFile('out').writeAsStringSync('hey'); + + await buildSystem.build('foo', environment, const BuildSystemConfig()); + expect(fooInvocations, 1); + })); + + + test('Re-invoke build if output is modified', () => testbed.run(() async { + await buildSystem.build('foo', environment, const BuildSystemConfig()); + + environment.buildDir.childFile('out').writeAsStringSync('Something different'); + + await buildSystem.build('foo', environment, const BuildSystemConfig()); + expect(fooInvocations, 2); + })); + + test('Runs dependencies of targets', () => testbed.run(() async { + await buildSystem.build('bar', environment, const BuildSystemConfig()); + + expect(fs.file(fs.path.join(environment.buildDir.path, 'bar')).existsSync(), true); + expect(fooInvocations, 1); + expect(barInvocations, 1); + })); + + test('handles a throwing build action', () => testbed.run(() async { + final BuildResult result = await buildSystem.build('fizz', environment, const BuildSystemConfig()); + + expect(result.hasException, true); + })); + + test('Can describe itself with JSON output', () => testbed.run(() { + environment.buildDir.createSync(recursive: true); + expect(fooTarget.toJson(environment), { + 'inputs': [ + '/foo.dart' + ], + 'outputs': [ + fs.path.join(environment.buildDir.path, 'out'), + ], + 'dependencies': [], + 'name': 'foo', + 'stamp': fs.path.join(environment.buildDir.path, 'foo.stamp'), + }); + })); + + test('Compute update recognizes added files', () => testbed.run(() async { + fs.directory('build').createSync(); + final FileHashStore fileCache = FileHashStore(environment); + fileCache.initialize(); + final List inputs = fooTarget.resolveInputs(environment); + final Map changes = await fooTarget.computeChanges(inputs, environment, fileCache); + fileCache.persist(); + + expect(changes, { + '/foo.dart': ChangeType.Added + }); + + await buildSystem.build('foo', environment, const BuildSystemConfig()); + final Map secondChanges = await fooTarget.computeChanges(inputs, environment, fileCache); + + expect(secondChanges, {}); + })); + }); + + group('FileCache', () { + Testbed testbed; + Environment environment; + + setUp(() { + testbed = Testbed(setup: () { + fs.directory('build').createSync(); + environment = Environment( + projectDir: fs.currentDirectory, + ); + }); + }); + + test('Initializes file cache', () => testbed.run(() { + final FileHashStore fileCache = FileHashStore(environment); + fileCache.initialize(); + fileCache.persist(); + + expect(fs.file(fs.path.join('build', '.filecache')).existsSync(), true); + + final List buffer = fs.file(fs.path.join('build', '.filecache')).readAsBytesSync(); + final pb.FileStorage fileStorage = pb.FileStorage.fromBuffer(buffer); + + expect(fileStorage.files, isEmpty); + expect(fileStorage.version, 1); + })); + + test('saves and restores to file cache', () => testbed.run(() { + final File file = fs.file('foo.dart') + ..createSync() + ..writeAsStringSync('hello'); + final FileHashStore fileCache = FileHashStore(environment); + fileCache.initialize(); + fileCache.hashFiles([file]); + fileCache.persist(); + final String currentHash = fileCache.currentHashes[file.resolveSymbolicLinksSync()]; + final List buffer = fs.file(fs.path.join('build', '.filecache')).readAsBytesSync(); + pb.FileStorage fileStorage = pb.FileStorage.fromBuffer(buffer); + + expect(fileStorage.files.single.hash, currentHash); + expect(fileStorage.files.single.path, file.resolveSymbolicLinksSync()); + + + final FileHashStore newFileCache = FileHashStore(environment); + newFileCache.initialize(); + expect(newFileCache.currentHashes, isEmpty); + expect(newFileCache.previousHashes[fs.path.absolute('foo.dart')], currentHash); + newFileCache.persist(); + + // Still persisted correctly. + fileStorage = pb.FileStorage.fromBuffer(buffer); + + expect(fileStorage.files.single.hash, currentHash); + expect(fileStorage.files.single.path, file.resolveSymbolicLinksSync()); + })); + }); + + group('Target', () { + Testbed testbed; + MockPlatform mockPlatform; + Environment environment; + Target sharedTarget; + BuildSystem buildSystem; + int shared; + + setUp(() { + shared = 0; + Cache.flutterRoot = ''; + mockPlatform = MockPlatform(); + // Keep file paths the same. + when(mockPlatform.isWindows).thenReturn(false); + when(mockPlatform.isLinux).thenReturn(true); + when(mockPlatform.isMacOS).thenReturn(false); + testbed = Testbed( + setup: () { + environment = Environment( + projectDir: fs.currentDirectory, + ); + fs.file('foo.dart').createSync(recursive: true); + fs.file('pubspec.yaml').createSync(); + sharedTarget = Target( + name: 'shared', + inputs: const [ + Source.pattern('{PROJECT_DIR}/foo.dart'), + ], + outputs: const [], + dependencies: [], + buildAction: (Map updates, Environment environment) { + shared += 1; + } + ); + final Target fooTarget = Target( + name: 'foo', + inputs: const [ + Source.pattern('{PROJECT_DIR}/foo.dart'), + ], + outputs: const [ + Source.pattern('{BUILD_DIR}/out'), + ], + dependencies: [sharedTarget], + buildAction: (Map updates, Environment environment) { + environment + .buildDir + .childFile('out') + ..createSync(recursive: true) + ..writeAsStringSync('hey'); + } + ); + final Target barTarget = Target( + name: 'bar', + inputs: const [ + Source.pattern('{BUILD_DIR}/out'), + ], + outputs: const [ + Source.pattern('{BUILD_DIR}/bar'), + ], + dependencies: [fooTarget, sharedTarget], + buildAction: (Map updates, Environment environment) { + environment + .buildDir + .childFile('bar') + ..createSync(recursive: true) + ..writeAsStringSync('there'); + } + ); + buildSystem = BuildSystem({ + fooTarget.name: fooTarget, + barTarget.name: barTarget, + sharedTarget.name: sharedTarget, + }); + }, + overrides: { + Platform: () => mockPlatform, + } + ); + }); + + test('Only invokes shared target once', () => testbed.run(() async { + await buildSystem.build('bar', environment, const BuildSystemConfig()); + + expect(shared, 1); + })); + }); + + group('Source', () { + Testbed testbed; + SourceVisitor visitor; + Environment environment; + + setUp(() { + testbed = Testbed(setup: () { + fs.directory('cache').createSync(); + environment = Environment( + projectDir: fs.currentDirectory, + buildDir: fs.directory('build'), + ); + visitor = SourceVisitor(environment); + environment.buildDir.createSync(recursive: true); + }); + }); + + test('configures implicit vs explict correctly', () => testbed.run(() { + expect(const Source.pattern('{PROJECT_DIR}/foo').implicit, false); + expect(const Source.pattern('{PROJECT_DIR}/*foo').implicit, true); + expect(Source.function((Environment environment) => []).implicit, true); + expect(Source.behavior(TestBehavior()).implicit, true); + })); + + test('can substitute {PROJECT_DIR}/foo', () => testbed.run(() { + fs.file('foo').createSync(); + const Source fooSource = Source.pattern('{PROJECT_DIR}/foo'); + fooSource.accept(visitor); + + expect(visitor.sources.single.path, fs.path.absolute('foo')); + })); + + test('can substitute {BUILD_DIR}/bar', () => testbed.run(() { + final String path = fs.path.join(environment.buildDir.path, 'bar'); + fs.file(path).createSync(); + const Source barSource = Source.pattern('{BUILD_DIR}/bar'); + barSource.accept(visitor); + + expect(visitor.sources.single.path, fs.path.absolute(path)); + })); + + test('can substitute Artifact', () => testbed.run(() { + final String path = fs.path.join( + Cache.instance.getArtifactDirectory('engine').path, + 'windows-x64', + 'foo', + ); + fs.file(path).createSync(recursive: true); + const Source fizzSource = Source.artifact(Artifact.windowsDesktopPath, platform: TargetPlatform.windows_x64); + fizzSource.accept(visitor); + + expect(visitor.sources.single.resolveSymbolicLinksSync(), fs.path.absolute(path)); + })); + + test('can substitute {PROJECT_DIR}/*.fizz', () => testbed.run(() { + const Source fizzSource = Source.pattern('{PROJECT_DIR}/*.fizz'); + fizzSource.accept(visitor); + + expect(visitor.sources, isEmpty); + + fs.file('foo.fizz').createSync(); + fs.file('foofizz').createSync(); + + + fizzSource.accept(visitor); + + expect(visitor.sources.single.path, fs.path.absolute('foo.fizz')); + })); + + test('can substitute {PROJECT_DIR}/fizz.*', () => testbed.run(() { + const Source fizzSource = Source.pattern('{PROJECT_DIR}/fizz.*'); + fizzSource.accept(visitor); + + expect(visitor.sources, isEmpty); + + fs.file('fizz.foo').createSync(); + fs.file('fizz').createSync(); + + fizzSource.accept(visitor); + + expect(visitor.sources.single.path, fs.path.absolute('fizz.foo')); + })); + + + test('can substitute {PROJECT_DIR}/a*bc', () => testbed.run(() { + const Source fizzSource = Source.pattern('{PROJECT_DIR}/bc*bc'); + fizzSource.accept(visitor); + + expect(visitor.sources, isEmpty); + + fs.file('bcbc').createSync(); + fs.file('bc').createSync(); + + fizzSource.accept(visitor); + + expect(visitor.sources.single.path, fs.path.absolute('bcbc')); + })); + + + test('crashes on bad substitute of two **', () => testbed.run(() { + const Source fizzSource = Source.pattern('{PROJECT_DIR}/*.*bar'); + + fs.file('abcd.bar').createSync(); + + expect(() => fizzSource.accept(visitor), throwsA(isInstanceOf())); + })); + + + test('can\'t substitute foo', () => testbed.run(() { + const Source invalidBase = Source.pattern('foo'); + + expect(() => invalidBase.accept(visitor), throwsA(isInstanceOf())); + })); + }); + + + + test('Can find dependency cycles', () { + final Target barTarget = Target( + name: 'bar', + inputs: [], + outputs: [], + buildAction: null, + dependencies: nonconst([]) + ); + final Target fooTarget = Target( + name: 'foo', + inputs: [], + outputs: [], + buildAction: null, + dependencies: nonconst([]) + ); + barTarget.dependencies.add(fooTarget); + fooTarget.dependencies.add(barTarget); + expect(() => checkCycles(barTarget), throwsA(isInstanceOf())); + }); +} + +class MockPlatform extends Mock implements Platform {} + +// Work-around for silly lint check. +T nonconst(T input) => input; + +class TestBehavior extends SourceBehavior { + @override + List inputs(Environment environment) { + return null; + } + + @override + List outputs(Environment environment) { + return null; + } +} diff --git a/packages/flutter_tools/test/build_system/exceptions_test.dart b/packages/flutter_tools/test/build_system/exceptions_test.dart new file mode 100644 index 00000000000..68486141e66 --- /dev/null +++ b/packages/flutter_tools/test/build_system/exceptions_test.dart @@ -0,0 +1,72 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/build_system/exceptions.dart'; + +import '../src/common.dart'; + +void main() { + test('Exceptions', () { + final MissingInputException missingInputException = MissingInputException( + [fs.file('foo'), fs.file('bar')], 'example'); + final CycleException cycleException = CycleException(const { + Target( + name: 'foo', + buildAction: null, + inputs: [], + outputs: [], + ), + Target( + name: 'bar', + buildAction: null, + inputs: [], + outputs: [], + ) + }); + final InvalidPatternException invalidPatternException = InvalidPatternException( + 'ABC' + ); + final MissingOutputException missingOutputException = MissingOutputException( + [ fs.file('foo'), fs.file('bar') ], + 'example' + ); + final MisplacedOutputException misplacedOutputException = MisplacedOutputException( + 'foo', + 'example', + ); + final MissingDefineException missingDefineException = MissingDefineException( + 'foobar', + 'example', + ); + + expect( + missingInputException.toString(), + 'foo, bar were declared as an inputs, ' + 'but did not exist. Check the definition of target:example for errors'); + expect( + cycleException.toString(), + 'Dependency cycle detected in build: foo -> bar' + ); + expect( + invalidPatternException.toString(), + 'The pattern "ABC" is not valid' + ); + expect( + missingOutputException.toString(), + 'foo, bar were declared as outputs, but were not generated by the ' + 'action. Check the definition of target:example for errors' + ); + expect( + misplacedOutputException.toString(), + 'Target example produced an output at foo which is outside of the ' + 'current build or project directory', + ); + expect( + missingDefineException.toString(), + 'Target example required define foobar but it was not provided' + ); + }); +} diff --git a/packages/flutter_tools/test/build_system/targets/assets_test.dart b/packages/flutter_tools/test/build_system/targets/assets_test.dart new file mode 100644 index 00000000000..43021558039 --- /dev/null +++ b/packages/flutter_tools/test/build_system/targets/assets_test.dart @@ -0,0 +1,70 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/build_system/targets/assets.dart'; + +import '../../src/common.dart'; +import '../../src/testbed.dart'; + +void main() { + group('copy_assets', () { + Testbed testbed; + BuildSystem buildSystem; + Environment environment; + + setUp(() { + testbed = Testbed(setup: () { + environment = Environment( + projectDir: fs.currentDirectory, + ); + buildSystem = BuildSystem({ + copyAssets.name: copyAssets, + }); + fs.file(fs.path.join('assets', 'foo', 'bar.png')) + ..createSync(recursive: true); + fs.file('.packages') + ..createSync(); + fs.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(''' +name: example + +flutter: + assets: + - assets/foo/bar.png +'''); + }); + }); + + test('Copies files to correct asset directory', () => testbed.run(() async { + await buildSystem.build('copy_assets', environment, const BuildSystemConfig()); + + expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'AssetManifest.json')).existsSync(), true); + expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'FontManifest.json')).existsSync(), true); + expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'LICENSE')).existsSync(), true); + // See https://github.com/flutter/flutter/issues/35293 + expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'assets/foo/bar.png')).existsSync(), true); + })); + + test('Does not leave stale files in build directory', () => testbed.run(() async { + await buildSystem.build('copy_assets', environment, const BuildSystemConfig()); + + expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'assets/foo/bar.png')).existsSync(), true); + // Modify manifest to remove asset. + fs.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(''' +name: example + +flutter: +'''); + await buildSystem.build('copy_assets', environment, const BuildSystemConfig()); + + // See https://github.com/flutter/flutter/issues/35293 + expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'assets/foo/bar.png')).existsSync(), false); + })); + }); +} diff --git a/packages/flutter_tools/test/build_system/targets/dart_test.dart b/packages/flutter_tools/test/build_system/targets/dart_test.dart new file mode 100644 index 00000000000..0d742efb3fe --- /dev/null +++ b/packages/flutter_tools/test/build_system/targets/dart_test.dart @@ -0,0 +1,224 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/base/build.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/build_system/exceptions.dart'; +import 'package:flutter_tools/src/build_system/targets/dart.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/compile.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; + +import '../../src/common.dart'; +import '../../src/mocks.dart'; +import '../../src/testbed.dart'; + +void main() { + group('dart rules', () { + Testbed testbed; + BuildSystem buildSystem; + Environment androidEnvironment; + Environment iosEnvironment; + MockProcessManager mockProcessManager; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + mockProcessManager = MockProcessManager(); + testbed = Testbed(setup: () { + androidEnvironment = Environment( + projectDir: fs.currentDirectory, + defines: { + kBuildMode: getNameForBuildMode(BuildMode.profile), + kTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm), + } + ); + iosEnvironment = Environment( + projectDir: fs.currentDirectory, + defines: { + kBuildMode: getNameForBuildMode(BuildMode.profile), + kTargetPlatform: getNameForTargetPlatform(TargetPlatform.ios), + } + ); + buildSystem = BuildSystem(); + HostPlatform hostPlatform; + if (platform.isWindows) { + hostPlatform = HostPlatform.windows_x64; + } else if (platform.isLinux) { + hostPlatform = HostPlatform.linux_x64; + } else if (platform.isMacOS) { + hostPlatform = HostPlatform.darwin_x64; + } else { + assert(false); + } + final String skyEngineLine = platform.isWindows + ? r'sky_engine:file:///C:/bin/cache/pkg/sky_engine/lib/' + : 'sky_engine:file:///bin/cache/pkg/sky_engine/lib/'; + fs.file('.packages') + ..createSync() + ..writeAsStringSync(''' +# Generated +$skyEngineLine +flutter_tools:lib/'''); + final String engineArtifacts = fs.path.join('bin', 'cache', + 'artifacts', 'engine'); + final List paths = [ + fs.path.join('bin', 'cache', 'pkg', 'sky_engine', 'lib', 'ui', + 'ui.dart'), + fs.path.join('bin', 'cache', 'pkg', 'sky_engine', 'sdk_ext', + 'vmservice_io.dart'), + fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'), + fs.path.join(engineArtifacts, getNameForHostPlatform(hostPlatform), + 'frontend_server.dart.snapshot'), + fs.path.join(engineArtifacts, 'android-arm-profile', + getNameForHostPlatform(hostPlatform), 'gen_snapshot'), + fs.path.join(engineArtifacts, 'ios-profile', 'gen_snapshot'), + fs.path.join(engineArtifacts, 'common', 'flutter_patched_sdk', + 'platform_strong.dill'), + fs.path.join('lib', 'foo.dart'), + fs.path.join('lib', 'bar.dart'), + fs.path.join('lib', 'fizz'), + ]; + for (String path in paths) { + fs.file(path).createSync(recursive: true); + } + }, overrides: { + KernelCompilerFactory: () => FakeKernelCompilerFactory(), + GenSnapshot: () => FakeGenSnapshot(), + }); + }); + + test('kernel_snapshot Produces correct output directory', () => testbed.run(() async { + await buildSystem.build('kernel_snapshot', androidEnvironment, const BuildSystemConfig()); + + expect(fs.file(fs.path.join(androidEnvironment.buildDir.path,'main.app.dill')).existsSync(), true); + })); + + test('kernel_snapshot throws error if missing build mode', () => testbed.run(() async { + final BuildResult result = await buildSystem.build('kernel_snapshot', + androidEnvironment..defines.remove(kBuildMode), const BuildSystemConfig()); + + expect(result.exceptions.values.single.exception, isInstanceOf()); + })); + + test('aot_elf_profile Produces correct output directory', () => testbed.run(() async { + await buildSystem.build('aot_elf_profile', androidEnvironment, const BuildSystemConfig()); + + expect(fs.file(fs.path.join(androidEnvironment.buildDir.path, 'main.app.dill')).existsSync(), true); + expect(fs.file(fs.path.join(androidEnvironment.buildDir.path, 'app.so')).existsSync(), true); + })); + + test('aot_elf_profile throws error if missing build mode', () => testbed.run(() async { + final BuildResult result = await buildSystem.build('aot_elf_profile', + androidEnvironment..defines.remove(kBuildMode), const BuildSystemConfig()); + + expect(result.exceptions.values.single.exception, isInstanceOf()); + })); + + + test('aot_elf_profile throws error if missing target platform', () => testbed.run(() async { + final BuildResult result = await buildSystem.build('aot_elf_profile', + androidEnvironment..defines.remove(kTargetPlatform), const BuildSystemConfig()); + + expect(result.exceptions.values.single.exception, isInstanceOf()); + })); + + + test('aot_assembly_profile throws error if missing build mode', () => testbed.run(() async { + final BuildResult result = await buildSystem.build('aot_assembly_profile', + iosEnvironment..defines.remove(kBuildMode), const BuildSystemConfig()); + + expect(result.exceptions.values.single.exception, isInstanceOf()); + })); + + test('aot_assembly_profile throws error if missing target platform', () => testbed.run(() async { + final BuildResult result = await buildSystem.build('aot_assembly_profile', + iosEnvironment..defines.remove(kTargetPlatform), const BuildSystemConfig()); + + expect(result.exceptions.values.single.exception, isInstanceOf()); + })); + + test('aot_assembly_profile throws error if built for non-iOS platform', () => testbed.run(() async { + final BuildResult result = await buildSystem.build('aot_assembly_profile', + androidEnvironment, const BuildSystemConfig()); + + expect(result.exceptions.values.single.exception, isInstanceOf()); + })); + + test('aot_assembly_profile will lipo binaries together when multiple archs are requested', () => testbed.run(() async { + iosEnvironment.defines[kIosArchs] ='armv7,arm64'; + when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async { + fs.file(fs.path.join(iosEnvironment.buildDir.path, 'App.framework', 'App')) + .createSync(recursive: true); + return FakeProcessResult( + stdout: '', + stderr: '', + ); + }); + final BuildResult result = await buildSystem.build('aot_assembly_profile', + iosEnvironment, const BuildSystemConfig()); + + expect(result.success, true); + }, overrides: { + ProcessManager: () => mockProcessManager, + })); + }); +} + +class MockProcessManager extends Mock implements ProcessManager {} + +class FakeGenSnapshot implements GenSnapshot { + @override + Future run({SnapshotType snapshotType, IOSArch iosArch, Iterable additionalArgs = const []}) async { + final Directory out = fs.file(additionalArgs.last).parent; + if (iosArch == null) { + out.childFile('app.so').createSync(); + out.childFile('gen_snapshot.d').createSync(); + return 0; + } + out.childDirectory('App.framework').childFile('App').createSync(recursive: true); + out.childFile('snapshot_assembly.S').createSync(); + out.childFile('snapshot_assembly.o').createSync(); + return 0; + } +} + +class FakeKernelCompilerFactory implements KernelCompilerFactory { + FakeKernelCompiler kernelCompiler = FakeKernelCompiler(); + + @override + Future create(FlutterProject flutterProject) async { + return kernelCompiler; + } +} + +class FakeKernelCompiler implements KernelCompiler { + @override + Future compile({ + String sdkRoot, + String mainPath, + String outputFilePath, + String depFilePath, + TargetModel targetModel = TargetModel.flutter, + bool linkPlatformKernelIn = false, + bool aot = false, + bool trackWidgetCreation, + List extraFrontEndOptions, + String incrementalCompilerByteStorePath, + String packagesPath, + List fileSystemRoots, + String fileSystemScheme, + bool targetProductVm = false, + String initializeFromDill}) async { + fs.file(outputFilePath).createSync(recursive: true); + return CompilerOutput(outputFilePath, 0, null); + } +} diff --git a/packages/flutter_tools/test/build_system/targets/linux_test.dart b/packages/flutter_tools/test/build_system/targets/linux_test.dart new file mode 100644 index 00000000000..4b22c9aee76 --- /dev/null +++ b/packages/flutter_tools/test/build_system/targets/linux_test.dart @@ -0,0 +1,84 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/build_system/targets/linux.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/testbed.dart'; + +void main() { + group('unpack_linux', () { + Testbed testbed; + BuildSystem buildSystem; + Environment environment; + MockPlatform mockPlatform; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + mockPlatform = MockPlatform(); + when(mockPlatform.isWindows).thenReturn(false); + when(mockPlatform.isMacOS).thenReturn(false); + when(mockPlatform.isLinux).thenReturn(true); + testbed = Testbed(setup: () { + Cache.flutterRoot = ''; + environment = Environment( + projectDir: fs.currentDirectory, + ); + buildSystem = BuildSystem({ + unpackLinux.name: unpackLinux, + }); + fs.file('bin/cache/artifacts/engine/linux-x64/libflutter_linux.so').createSync(recursive: true); + fs.file('bin/cache/artifacts/engine/linux-x64/flutter_export.h').createSync(); + fs.file('bin/cache/artifacts/engine/linux-x64/flutter_messenger.h').createSync(); + fs.file('bin/cache/artifacts/engine/linux-x64/flutter_plugin_registrar.h').createSync(); + fs.file('bin/cache/artifacts/engine/linux-x64/flutter_glfw.h').createSync(); + fs.file('bin/cache/artifacts/engine/linux-x64/icudtl.dat').createSync(); + fs.file('bin/cache/artifacts/engine/linux-x64/cpp_client_wrapper/foo').createSync(recursive: true); + fs.directory('linux').createSync(); + }, overrides: { + Platform: () => mockPlatform, + }); + }); + + test('Copies files to correct cache directory', () => testbed.run(() async { + final BuildResult result = await buildSystem.build('unpack_linux', environment, const BuildSystemConfig()); + + expect(result.hasException, false); + expect(fs.file('linux/flutter/libflutter_linux.so').existsSync(), true); + expect(fs.file('linux/flutter/flutter_export.h').existsSync(), true); + expect(fs.file('linux/flutter/flutter_messenger.h').existsSync(), true); + expect(fs.file('linux/flutter/flutter_plugin_registrar.h').existsSync(), true); + expect(fs.file('linux/flutter/flutter_glfw.h').existsSync(), true); + expect(fs.file('linux/flutter/icudtl.dat').existsSync(), true); + expect(fs.file('linux/flutter/cpp_client_wrapper/foo').existsSync(), true); + })); + + test('Does not re-copy files unecessarily', () => testbed.run(() async { + await buildSystem.build('unpack_linux', environment, const BuildSystemConfig()); + final DateTime modified = fs.file('linux/flutter/libflutter_linux.so').statSync().modified; + await buildSystem.build('unpack_linux', environment, const BuildSystemConfig()); + + expect(fs.file('linux/flutter/libflutter_linux.so').statSync().modified, equals(modified)); + })); + + test('Detects changes in input cache files', () => testbed.run(() async { + await buildSystem.build('unpack_linux', environment, const BuildSystemConfig()); + fs.file('bin/cache/artifacts/engine/linux-x64/libflutter_linux.so').writeAsStringSync('asd'); // modify cache. + + await buildSystem.build('unpack_linux', environment, const BuildSystemConfig()); + + expect(fs.file('linux/flutter/libflutter_linux.so').readAsStringSync(), 'asd'); + })); + }); +} + +class MockPlatform extends Mock implements Platform {} diff --git a/packages/flutter_tools/test/build_system/targets/macos_test.dart b/packages/flutter_tools/test/build_system/targets/macos_test.dart new file mode 100644 index 00000000000..5deadb5342a --- /dev/null +++ b/packages/flutter_tools/test/build_system/targets/macos_test.dart @@ -0,0 +1,115 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/process_manager.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/build_system/targets/macos.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; + +import '../../src/common.dart'; +import '../../src/testbed.dart'; + +void main() { + group('unpack_macos', () { + Testbed testbed; + BuildSystem buildSystem; + Environment environment; + MockPlatform mockPlatform; + + setUp(() { + mockPlatform = MockPlatform(); + when(mockPlatform.isWindows).thenReturn(false); + when(mockPlatform.isMacOS).thenReturn(true); + when(mockPlatform.isLinux).thenReturn(false); + testbed = Testbed(setup: () { + environment = Environment( + projectDir: fs.currentDirectory, + ); + buildSystem = BuildSystem({ + unpackMacos.name: unpackMacos, + }); + final List inputs = [ + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/FlutterMacOS'), + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEOpenGLContextHandling.h'), + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEReshapeListener.h'), + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEView.h'), + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEViewController.h'), + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterBinaryMessenger.h'), + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterChannels.h'), + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterCodecs.h'), + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterMacOS.h'), + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterPluginMacOS.h'), + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterPluginRegisrarMacOS.h'), + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Modules/module.modulemap'), + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Resources/icudtl.dat'), + fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Resources/info.plist'), + ]; + for (File input in inputs) { + input.createSync(recursive: true); + } + when(processManager.runSync(any)).thenAnswer((Invocation invocation) { + final List arguments = invocation.positionalArguments.first; + final Directory source = fs.directory(arguments[arguments.length - 2]); + final Directory target = fs.directory(arguments.last) + ..createSync(recursive: true); + for (FileSystemEntity entity in source.listSync(recursive: true)) { + if (entity is File) { + final String relative = fs.path.relative(entity.path, from: source.path); + final String destination = fs.path.join(target.path, relative); + if (!fs.file(destination).parent.existsSync()) { + fs.file(destination).parent.createSync(); + } + entity.copySync(destination); + } + } + return FakeProcessResult()..exitCode = 0; + }); + }, overrides: { + ProcessManager: () => MockProcessManager(), + Platform: () => mockPlatform, + }); + }); + + test('Copies files to correct cache directory', () => testbed.run(() async { + await buildSystem.build('unpack_macos', environment, const BuildSystemConfig()); + + expect(fs.directory('macos/Flutter/FlutterMacOS.framework').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/FlutterMacOS').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEOpenGLContextHandling.h').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEReshapeListener.h').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEView.h').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEViewController.h').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterBinaryMessenger.h').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterChannels.h').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterCodecs.h').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterMacOS.h').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterPluginMacOS.h').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterPluginRegisrarMacOS.h').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/Modules/module.modulemap').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/Resources/icudtl.dat').existsSync(), true); + expect(fs.file('macos/Flutter/FlutterMacOS.framework/Resources/info.plist').existsSync(), true); + })); + }); +} + +class MockPlatform extends Mock implements Platform {} + +class MockProcessManager extends Mock implements ProcessManager {} +class FakeProcessResult implements ProcessResult { + @override + int exitCode; + + @override + int pid = 0; + + @override + String stderr = ''; + + @override + String stdout = ''; +} diff --git a/packages/flutter_tools/test/build_system/targets/windows_test.dart b/packages/flutter_tools/test/build_system/targets/windows_test.dart new file mode 100644 index 00000000000..f46896ad4ec --- /dev/null +++ b/packages/flutter_tools/test/build_system/targets/windows_test.dart @@ -0,0 +1,97 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/build_system/targets/windows.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/testbed.dart'; + +void main() { + group('unpack_windows', () { + Testbed testbed; + BuildSystem buildSystem; + Environment environment; + Platform platform; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + Cache.flutterRoot = ''; + platform = MockPlatform(); + when(platform.isWindows).thenReturn(true); + when(platform.isMacOS).thenReturn(false); + when(platform.isLinux).thenReturn(false); + when(platform.pathSeparator).thenReturn(r'\'); + testbed = Testbed(setup: () { + environment = Environment( + projectDir: fs.currentDirectory, + ); + buildSystem = BuildSystem({ + unpackWindows.name: unpackWindows, + }); + fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_export.h').createSync(recursive: true); + fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_messenger.h').createSync(); + fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_windows.dll').createSync(); + fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_windows.dll.exp').createSync(); + fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_windows.dll.lib').createSync(); + fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_windows.dll.pdb').createSync(); + fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\lutter_export.h').createSync(); + fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_messenger.h').createSync(); + fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_plugin_registrar.h').createSync(); + fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_glfw.h').createSync(); + fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\icudtl.dat').createSync(); + fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\cpp_client_wrapper\foo').createSync(recursive: true); + fs.directory('windows').createSync(); + }, overrides: { + FileSystem: () => MemoryFileSystem(style: FileSystemStyle.windows), + Platform: () => platform, + }); + }); + + test('Copies files to correct cache directory', () => testbed.run(() async { + await buildSystem.build('unpack_windows', environment, const BuildSystemConfig()); + + expect(fs.file(r'C:\windows\flutter\flutter_export.h').existsSync(), true); + expect(fs.file(r'C:\windows\flutter\flutter_messenger.h').existsSync(), true); + expect(fs.file(r'C:\windows\flutter\flutter_windows.dll').existsSync(), true); + expect(fs.file(r'C:\windows\flutter\flutter_windows.dll.exp').existsSync(), true); + expect(fs.file(r'C:\windows\flutter\flutter_windows.dll.lib').existsSync(), true); + expect(fs.file(r'C:\windows\flutter\flutter_windows.dll.pdb').existsSync(), true); + expect(fs.file(r'C:\windows\flutter\flutter_export.h').existsSync(), true); + expect(fs.file(r'C:\windows\flutter\flutter_messenger.h').existsSync(), true); + expect(fs.file(r'C:\windows\flutter\flutter_plugin_registrar.h').existsSync(), true); + expect(fs.file(r'C:\windows\flutter\flutter_glfw.h').existsSync(), true); + expect(fs.file(r'C:\windows\flutter\icudtl.dat').existsSync(), true); + expect(fs.file(r'C:\windows\flutter\cpp_client_wrapper\foo').existsSync(), true); + })); + + test('Does not re-copy files unecessarily', () => testbed.run(() async { + await buildSystem.build('unpack_windows', environment, const BuildSystemConfig()); + final DateTime modified = fs.file(r'C:\windows\flutter\flutter_export.h').statSync().modified; + await buildSystem.build('unpack_windows', environment, const BuildSystemConfig()); + + expect(fs.file(r'C:\windows\flutter\flutter_export.h').statSync().modified, equals(modified)); + })); + + test('Detects changes in input cache files', () => testbed.run(() async { + await buildSystem.build('unpack_windows', environment, const BuildSystemConfig()); + final DateTime modified = fs.file(r'C:\windows\flutter\flutter_export.h').statSync().modified; + fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_export.h').writeAsStringSync('asd'); // modify cache. + + await buildSystem.build('unpack_windows', environment, const BuildSystemConfig()); + + expect(fs.file(r'C:\windows\flutter\flutter_export.h').statSync().modified, isNot(modified)); + })); + }); +} + +class MockPlatform extends Mock implements Platform {} diff --git a/packages/flutter_tools/test/commands/assemble_test.dart b/packages/flutter_tools/test/commands/assemble_test.dart new file mode 100644 index 00000000000..366eda77805 --- /dev/null +++ b/packages/flutter_tools/test/commands/assemble_test.dart @@ -0,0 +1,84 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:args/command_runner.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/assemble.dart'; +import 'package:flutter_tools/src/globals.dart'; +import 'package:mockito/mockito.dart'; + +import '../src/common.dart'; +import '../src/testbed.dart'; + +void main() { + group('Assemble', () { + Testbed testbed; + MockBuildSystem mockBuildSystem; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + mockBuildSystem = MockBuildSystem(); + testbed = Testbed(overrides: { + BuildSystem: () => mockBuildSystem, + }); + }); + + test('Can list the output directory relative to project root', () => testbed.run(() async { + final CommandRunner commandRunner = createTestCommandRunner(AssembleCommand()); + await commandRunner.run(['assemble', '--flutter-root=.', 'build-dir', '-dBuildMode=debug']); + final BufferLogger bufferLogger = logger; + final Environment environment = Environment( + defines: { + 'BuildMode': 'debug' + }, projectDir: fs.currentDirectory, + buildDir: fs.directory(getBuildDirectory()), + ); + + expect(bufferLogger.statusText.trim(), + fs.path.relative(environment.buildDir.path, from: fs.currentDirectory.path)); + })); + + test('Can describe a target', () => testbed.run(() async { + when(mockBuildSystem.describe('foobar', any)).thenReturn(>[ + {'fizz': 'bar'}, + ]); + final CommandRunner commandRunner = createTestCommandRunner(AssembleCommand()); + await commandRunner.run(['assemble', '--flutter-root=.', 'describe', 'foobar']); + final BufferLogger bufferLogger = logger; + + expect(bufferLogger.statusText.trim(), '[{"fizz":"bar"}]'); + })); + + test('Can describe a target\'s inputs', () => testbed.run(() async { + when(mockBuildSystem.describe('foobar', any)).thenReturn(>[ + {'name': 'foobar', 'inputs': ['bar', 'baz']}, + ]); + final CommandRunner commandRunner = createTestCommandRunner(AssembleCommand()); + await commandRunner.run(['assemble', '--flutter-root=.', 'inputs', 'foobar']); + final BufferLogger bufferLogger = logger; + + expect(bufferLogger.statusText.trim(), 'bar\nbaz'); + })); + + test('Can run a build', () => testbed.run(() async { + when(mockBuildSystem.build('foobar', any, any)).thenAnswer((Invocation invocation) async { + return BuildResult(true, const {}, const {}); + }); + final CommandRunner commandRunner = createTestCommandRunner(AssembleCommand()); + await commandRunner.run(['assemble', 'run', 'foobar']); + final BufferLogger bufferLogger = logger; + + expect(bufferLogger.statusText.trim(), 'build succeeded'); + })); + }); +} + +class MockBuildSystem extends Mock implements BuildSystem {} diff --git a/packages/flutter_tools/test/commands/attach_test.dart b/packages/flutter_tools/test/commands/attach_test.dart index 769f31593f9..ecd9df9b2e8 100644 --- a/packages/flutter_tools/test/commands/attach_test.dart +++ b/packages/flutter_tools/test/commands/attach_test.dart @@ -24,15 +24,18 @@ import '../src/context.dart'; import '../src/mocks.dart'; void main() { - final StreamLogger logger = StreamLogger(); group('attach', () { - final FileSystem testFileSystem = MemoryFileSystem( - style: platform.isWindows ? FileSystemStyle.windows : FileSystemStyle - .posix, - ); + StreamLogger logger; + FileSystem testFileSystem; setUp(() { Cache.disableLocking(); + logger = StreamLogger(); + testFileSystem = MemoryFileSystem( + style: platform.isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix, + ); testFileSystem.directory('lib').createSync(); testFileSystem.file(testFileSystem.path.join('lib', 'main.dart')).createSync(); }); @@ -108,7 +111,8 @@ void main() { const String outputDill = '/tmp/output.dill'; final MockHotRunner mockHotRunner = MockHotRunner(); - when(mockHotRunner.attach()).thenAnswer((_) async => 0); + when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter'))) + .thenAnswer((_) async => 0); final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory(); when( @@ -119,7 +123,7 @@ void main() { dillOutputPath: anyNamed('dillOutputPath'), debuggingOptions: anyNamed('debuggingOptions'), packagesFilePath: anyNamed('packagesFilePath'), - usesTerminalUI: anyNamed('usesTerminalUI'), + usesTerminalUi: anyNamed('usesTerminalUi'), flutterProject: anyNamed('flutterProject'), ipv6: false, ), @@ -151,7 +155,7 @@ void main() { dillOutputPath: outputDill, debuggingOptions: anyNamed('debuggingOptions'), packagesFilePath: anyNamed('packagesFilePath'), - usesTerminalUI: anyNamed('usesTerminalUI'), + usesTerminalUi: anyNamed('usesTerminalUi'), flutterProject: anyNamed('flutterProject'), ipv6: false, ), @@ -219,14 +223,14 @@ void main() { .thenReturn([ForwardedPort(hostPort, devicePort)]); when(portForwarder.unforward(any)) .thenAnswer((_) async => null); - when(mockHotRunner.attach()) - .thenAnswer((_) async => 0); + when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter'))) + .thenAnswer((_) async => 0); when(mockHotRunnerFactory.build( any, target: anyNamed('target'), debuggingOptions: anyNamed('debuggingOptions'), packagesFilePath: anyNamed('packagesFilePath'), - usesTerminalUI: anyNamed('usesTerminalUI'), + usesTerminalUi: anyNamed('usesTerminalUi'), flutterProject: anyNamed('flutterProject'), ipv6: false, )).thenReturn(mockHotRunner); @@ -256,7 +260,7 @@ void main() { target: foo.path, debuggingOptions: anyNamed('debuggingOptions'), packagesFilePath: anyNamed('packagesFilePath'), - usesTerminalUI: anyNamed('usesTerminalUI'), + usesTerminalUi: anyNamed('usesTerminalUi'), flutterProject: anyNamed('flutterProject'), ipv6: false, )).called(1); diff --git a/packages/flutter_tools/test/resident_runner_test.dart b/packages/flutter_tools/test/resident_runner_test.dart index a4c96b39d71..024d08a8470 100644 --- a/packages/flutter_tools/test/resident_runner_test.dart +++ b/packages/flutter_tools/test/resident_runner_test.dart @@ -3,91 +3,114 @@ // found in the LICENSE file. import 'dart:async'; + import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/resident_runner.dart'; +import 'package:flutter_tools/src/run_hot.dart'; +import 'package:flutter_tools/src/vmservice.dart'; import 'package:mockito/mockito.dart'; import 'src/common.dart'; -import 'src/context.dart'; - -class TestRunner extends ResidentRunner { - TestRunner(List devices) - : super(devices); - - bool hasHelpBeenPrinted = false; - String receivedCommand; - - @override - Future cleanupAfterSignal() async { } - - @override - Future cleanupAtFinish() async { } - - @override - Future handleTerminalCommand(String code) async { - receivedCommand = code; - } - - @override - void printHelp({ bool details }) { - hasHelpBeenPrinted = true; - } - - @override - Future run({ - Completer connectionInfoCompleter, - Completer appStartedCompleter, - String route, - bool shouldBuild = true, - }) async => null; - - @override - Future attach({ - Completer connectionInfoCompleter, - Completer appStartedCompleter, - }) async => null; -} +import 'src/testbed.dart'; void main() { - TestRunner createTestRunner() { - // TODO(jacobr): make these tests run with `trackWidgetCreation: true` as - // well as the default flags. - return TestRunner( - [FlutterDevice(MockDevice(), trackWidgetCreation: false, buildMode: BuildMode.debug)], - ); - } + group('ResidentRunner', () { + final Uri testUri = Uri.parse('foo://bar'); + Testbed testbed; + MockDevice mockDevice; + MockVMService mockVMService; + MockDevFS mockDevFS; + ResidentRunner residentRunner; - group('keyboard input handling', () { - testUsingContext('single help character', () async { - final TestRunner testRunner = createTestRunner(); - expect(testRunner.hasHelpBeenPrinted, isFalse); - await testRunner.processTerminalInput('h'); - expect(testRunner.hasHelpBeenPrinted, isTrue); - }); - testUsingContext('help character surrounded with newlines', () async { - final TestRunner testRunner = createTestRunner(); - expect(testRunner.hasHelpBeenPrinted, isFalse); - await testRunner.processTerminalInput('\nh\n'); - expect(testRunner.hasHelpBeenPrinted, isTrue); - }); - testUsingContext('reload character with trailing newline', () async { - final TestRunner testRunner = createTestRunner(); - expect(testRunner.receivedCommand, isNull); - await testRunner.processTerminalInput('r\n'); - expect(testRunner.receivedCommand, equals('r')); - }); - testUsingContext('newlines', () async { - final TestRunner testRunner = createTestRunner(); - expect(testRunner.receivedCommand, isNull); - await testRunner.processTerminalInput('\n\n'); - expect(testRunner.receivedCommand, equals('')); + setUp(() { + testbed = Testbed(setup: () { + residentRunner = HotRunner( + [ + mockDevice, + ], + stayResident: false, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + ); + }); + mockDevice = MockDevice(); + mockVMService = MockVMService(); + mockDevFS = MockDevFS(); + // DevFS Mocks + when(mockDevFS.lastCompiled).thenReturn(DateTime(2000)); + when(mockDevFS.sources).thenReturn([]); + when(mockDevFS.destroy()).thenAnswer((Invocation invocation) async { }); + // FlutterDevice Mocks. + when(mockDevice.updateDevFS( + // Intentionally provide empty list to match above mock. + invalidatedFiles: [], + mainPath: anyNamed('mainPath'), + target: anyNamed('target'), + bundle: anyNamed('bundle'), + firstBuildTime: anyNamed('firstBuildTime'), + bundleFirstUpload: anyNamed('bundleFirstUpload'), + bundleDirty: anyNamed('bundleDirty'), + fullRestart: anyNamed('fullRestart'), + projectRootPath: anyNamed('projectRootPath'), + pathToReload: anyNamed('pathToReload'), + )).thenAnswer((Invocation invocation) async { + return UpdateFSReport( + success: true, + syncedBytes: 0, + invalidatedSourcesCount: 0, + ); + }); + when(mockDevice.devFS).thenReturn(mockDevFS); + when(mockDevice.views).thenReturn([ + MockFlutterView(), + ]); + when(mockDevice.stopEchoingDeviceLog()).thenAnswer((Invocation invocation) async { }); + when(mockDevice.observatoryUris).thenReturn([ + testUri, + ]); + when(mockDevice.connect( + reloadSources: anyNamed('reloadSources'), + restart: anyNamed('restart'), + compileExpression: anyNamed('compileExpression') + )).thenAnswer((Invocation invocation) async { }); + when(mockDevice.setupDevFS(any, any, packagesFilePath: anyNamed('packagesFilePath'))) + .thenAnswer((Invocation invocation) async { + return testUri; + }); + when(mockDevice.vmServices).thenReturn([ + mockVMService, + ]); + when(mockDevice.refreshViews()).thenAnswer((Invocation invocation) async { }); + // VMService mocks. + when(mockVMService.wsAddress).thenReturn(testUri); + when(mockVMService.done).thenAnswer((Invocation invocation) { + final Completer result = Completer.sync(); + return result.future; + }); }); + + test('Can attach to device successfully', () => testbed.run(() async { + final Completer onConnectionInfo = Completer.sync(); + final Completer onAppStart = Completer.sync(); + final Future result = residentRunner.attach( + appStartedCompleter: onAppStart, + connectionInfoCompleter: onConnectionInfo, + ); + final Future connectionInfo = onConnectionInfo.future; + + expect(await result, 0); + + verify(mockDevice.initLogReader()).called(1); + + expect(onConnectionInfo.isCompleted, true); + expect((await connectionInfo).baseUri, 'foo://bar'); + expect(onAppStart.isCompleted, true); + })); }); } -class MockDevice extends Mock implements Device { - MockDevice() { - when(isSupported()).thenReturn(true); - } -} +class MockDevice extends Mock implements FlutterDevice {} +class MockFlutterView extends Mock implements FlutterView {} +class MockVMService extends Mock implements VMService {} +class MockDevFS extends Mock implements DevFS {} diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart index ee01dbf78d4..3714684bf23 100644 --- a/packages/flutter_tools/test/src/testbed.dart +++ b/packages/flutter_tools/test/src/testbed.dart @@ -76,7 +76,7 @@ class Testbed { : _setup = setup, _overrides = overrides; - final Future Function() _setup; + final FutureOr Function() _setup; final Map _overrides; /// Runs `test` within a tool zone. diff --git a/packages/flutter_tools/test/terminal_handler_test.dart b/packages/flutter_tools/test/terminal_handler_test.dart new file mode 100644 index 00000000000..6e182ba605b --- /dev/null +++ b/packages/flutter_tools/test/terminal_handler_test.dart @@ -0,0 +1,406 @@ +// Copyright 2017 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 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/globals.dart'; +import 'package:flutter_tools/src/resident_runner.dart'; +import 'package:flutter_tools/src/vmservice.dart'; +import 'package:mockito/mockito.dart'; + +import 'src/common.dart'; +import 'src/context.dart'; + +void main() { + TestRunner createTestRunner() { + // TODO(jacobr): make these tests run with `trackWidgetCreation: true` as + // well as the default flags. + return TestRunner( + [FlutterDevice(MockDevice(), trackWidgetCreation: false, buildMode: BuildMode.debug)], + ); + } + + group('keyboard input handling', () { + testUsingContext('single help character', () async { + final TestRunner testRunner = createTestRunner(); + final TerminalHandler terminalHandler = TerminalHandler(testRunner); + expect(testRunner.hasHelpBeenPrinted, false); + await terminalHandler.processTerminalInput('h'); + expect(testRunner.hasHelpBeenPrinted, true); + }); + + testUsingContext('help character surrounded with newlines', () async { + final TestRunner testRunner = createTestRunner(); + final TerminalHandler terminalHandler = TerminalHandler(testRunner); + expect(testRunner.hasHelpBeenPrinted, false); + await terminalHandler.processTerminalInput('\nh\n'); + expect(testRunner.hasHelpBeenPrinted, true); + }); + }); + + group('keycode verification, brought to you by the letter', () { + MockResidentRunner mockResidentRunner; + TerminalHandler terminalHandler; + + setUp(() { + mockResidentRunner = MockResidentRunner(); + terminalHandler = TerminalHandler(mockResidentRunner); + when(mockResidentRunner.supportsServiceProtocol).thenReturn(true); + }); + + testUsingContext('a, can handle trailing newlines', () async { + await terminalHandler.processTerminalInput('a\n'); + + expect(terminalHandler.lastReceivedCommand, 'a'); + }); + + testUsingContext('n, can handle trailing only newlines', () async { + await terminalHandler.processTerminalInput('\n\n'); + + expect(terminalHandler.lastReceivedCommand, ''); + }); + + testUsingContext('a - debugToggleProfileWidgetBuilds with service protocol', () async { + await terminalHandler.processTerminalInput('a'); + + verify(mockResidentRunner.debugToggleProfileWidgetBuilds()).called(1); + }); + + testUsingContext('a - debugToggleProfileWidgetBuilds without service protocol', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); + await terminalHandler.processTerminalInput('a'); + + verifyNever(mockResidentRunner.debugToggleProfileWidgetBuilds()); + }); + + + testUsingContext('a - debugToggleProfileWidgetBuilds', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(true); + await terminalHandler.processTerminalInput('a'); + + verify(mockResidentRunner.debugToggleProfileWidgetBuilds()).called(1); + }); + + testUsingContext('d,D - detach', () async { + await terminalHandler.processTerminalInput('d'); + await terminalHandler.processTerminalInput('D'); + + verify(mockResidentRunner.detach()).called(2); + }); + + testUsingContext('h,H,? - printHelp', () async { + await terminalHandler.processTerminalInput('h'); + await terminalHandler.processTerminalInput('H'); + await terminalHandler.processTerminalInput('?'); + + verify(mockResidentRunner.printHelp(details: true)).called(3); + }); + + testUsingContext('i, I - debugToggleWidgetInspector with service protocol', () async { + await terminalHandler.processTerminalInput('i'); + await terminalHandler.processTerminalInput('I'); + + verify(mockResidentRunner.debugToggleWidgetInspector()).called(2); + }); + + testUsingContext('i, I - debugToggleWidgetInspector without service protocol', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); + await terminalHandler.processTerminalInput('i'); + await terminalHandler.processTerminalInput('I'); + + verifyNever(mockResidentRunner.debugToggleWidgetInspector()); + }); + + testUsingContext('l - list flutter views', () async { + final MockFlutterDevice mockFlutterDevice = MockFlutterDevice(); + when(mockResidentRunner.isRunningDebug).thenReturn(true); + when(mockResidentRunner.flutterDevices).thenReturn([mockFlutterDevice]); + when(mockFlutterDevice.views).thenReturn([]); + + await terminalHandler.processTerminalInput('l'); + + final BufferLogger bufferLogger = logger; + + expect(bufferLogger.statusText, contains('Connected views:\n')); + }); + + testUsingContext('L - debugDumpLayerTree with service protocol', () async { + await terminalHandler.processTerminalInput('L'); + + verify(mockResidentRunner.debugDumpLayerTree()).called(1); + }); + + testUsingContext('L - debugDumpLayerTree without service protocol', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); + await terminalHandler.processTerminalInput('L'); + + verifyNever(mockResidentRunner.debugDumpLayerTree()); + }); + + testUsingContext('o,O - debugTogglePlatform with service protocol and debug mode', () async { + when(mockResidentRunner.isRunningDebug).thenReturn(true); + await terminalHandler.processTerminalInput('o'); + await terminalHandler.processTerminalInput('O'); + + verify(mockResidentRunner.debugTogglePlatform()).called(2); + }); + + testUsingContext('o,O - debugTogglePlatform without service protocol', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); + when(mockResidentRunner.isRunningDebug).thenReturn(true); + await terminalHandler.processTerminalInput('o'); + await terminalHandler.processTerminalInput('O'); + + verifyNever(mockResidentRunner.debugTogglePlatform()); + }); + + testUsingContext('p - debugToggleDebugPaintSizeEnabled with service protocol and debug mode', () async { + when(mockResidentRunner.isRunningDebug).thenReturn(true); + await terminalHandler.processTerminalInput('p'); + + verify(mockResidentRunner.debugToggleDebugPaintSizeEnabled()).called(1); + }); + + testUsingContext('p - debugTogglePlatform without service protocol', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); + when(mockResidentRunner.isRunningDebug).thenReturn(true); + await terminalHandler.processTerminalInput('p'); + + verifyNever(mockResidentRunner.debugToggleDebugPaintSizeEnabled()); + }); + + testUsingContext('p - debugToggleDebugPaintSizeEnabled with service protocol and debug mode', () async { + when(mockResidentRunner.isRunningDebug).thenReturn(true); + await terminalHandler.processTerminalInput('p'); + + verify(mockResidentRunner.debugToggleDebugPaintSizeEnabled()).called(1); + }); + + testUsingContext('p - debugTogglePlatform without service protocol', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); + when(mockResidentRunner.isRunningDebug).thenReturn(true); + await terminalHandler.processTerminalInput('p'); + + verifyNever(mockResidentRunner.debugToggleDebugPaintSizeEnabled()); + }); + + testUsingContext('P - debugTogglePerformanceOverlayOverride with service protocol', () async { + await terminalHandler.processTerminalInput('P'); + + verify(mockResidentRunner.debugTogglePerformanceOverlayOverride()).called(1); + }); + + testUsingContext('P - debugTogglePerformanceOverlayOverride without service protocol', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); + await terminalHandler.processTerminalInput('P'); + + verifyNever(mockResidentRunner.debugTogglePerformanceOverlayOverride()); + }); + + testUsingContext('q,Q - exit', () async { + await terminalHandler.processTerminalInput('q'); + await terminalHandler.processTerminalInput('Q'); + + verify(mockResidentRunner.exit()).called(2); + }); + + testUsingContext('s - screenshot', () async { + final MockDevice mockDevice = MockDevice(); + final MockFlutterDevice mockFlutterDevice = MockFlutterDevice(); + when(mockResidentRunner.isRunningDebug).thenReturn(true); + when(mockResidentRunner.flutterDevices).thenReturn([mockFlutterDevice]); + when(mockFlutterDevice.device).thenReturn(mockDevice); + when(mockDevice.supportsScreenshot).thenReturn(true); + + await terminalHandler.processTerminalInput('s'); + + verify(mockResidentRunner.screenshot(mockFlutterDevice)).called(1); + }); + + testUsingContext('r - hotReload supported and succeeds', () async { + when(mockResidentRunner.canHotReload).thenReturn(true); + when(mockResidentRunner.restart(fullRestart: false)) + .thenAnswer((Invocation invocation) async { + return OperationResult(0, ''); + }); + await terminalHandler.processTerminalInput('r'); + + verify(mockResidentRunner.restart(fullRestart: false)).called(1); + }); + + testUsingContext('r - hotReload supported and fails', () async { + when(mockResidentRunner.canHotReload).thenReturn(true); + when(mockResidentRunner.restart(fullRestart: false)) + .thenAnswer((Invocation invocation) async { + return OperationResult(1, ''); + }); + await terminalHandler.processTerminalInput('r'); + + verify(mockResidentRunner.restart(fullRestart: false)).called(1); + + final BufferLogger bufferLogger = logger; + + expect(bufferLogger.statusText, contains('Try again after fixing the above error(s).')); + }); + + testUsingContext('r - hotReload unsupported', () async { + when(mockResidentRunner.canHotReload).thenReturn(false); + await terminalHandler.processTerminalInput('r'); + + verifyNever(mockResidentRunner.restart(fullRestart: false)); + }); + + testUsingContext('R - hotRestart supported and succeeds', () async { + when(mockResidentRunner.canHotRestart).thenReturn(true); + when(mockResidentRunner.hotMode).thenReturn(true); + when(mockResidentRunner.restart(fullRestart: true)) + .thenAnswer((Invocation invocation) async { + return OperationResult(0, ''); + }); + await terminalHandler.processTerminalInput('R'); + + verify(mockResidentRunner.restart(fullRestart: true)).called(1); + }); + + testUsingContext('R - hotRestart supported and fails', () async { + when(mockResidentRunner.canHotRestart).thenReturn(true); + when(mockResidentRunner.hotMode).thenReturn(true); + when(mockResidentRunner.restart(fullRestart: true)) + .thenAnswer((Invocation invocation) async { + return OperationResult(1, 'fail'); + }); + await terminalHandler.processTerminalInput('R'); + + verify(mockResidentRunner.restart(fullRestart: true)).called(1); + + final BufferLogger bufferLogger = logger; + + expect(bufferLogger.statusText, contains('Try again after fixing the above error(s).')); + }); + + testUsingContext('R - hot restart unsupported', () async { + when(mockResidentRunner.canHotRestart).thenReturn(false); + await terminalHandler.processTerminalInput('R'); + + verifyNever(mockResidentRunner.restart(fullRestart: true)); + }); + + testUsingContext('S - debugDumpSemanticsTreeInTraversalOrder with service protocol', () async { + await terminalHandler.processTerminalInput('S'); + + verify(mockResidentRunner.debugDumpSemanticsTreeInTraversalOrder()).called(1); + }); + + testUsingContext('S - debugDumpSemanticsTreeInTraversalOrder without service protocol', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); + await terminalHandler.processTerminalInput('S'); + + verifyNever(mockResidentRunner.debugDumpSemanticsTreeInTraversalOrder()); + }); + + testUsingContext('t,T - debugDumpRenderTree with service protocol', () async { + await terminalHandler.processTerminalInput('t'); + await terminalHandler.processTerminalInput('T'); + + verify(mockResidentRunner.debugDumpRenderTree()).called(2); + }); + + testUsingContext('t,T - debugDumpSemanticsTreeInTraversalOrder without service protocol', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); + await terminalHandler.processTerminalInput('t'); + await terminalHandler.processTerminalInput('T'); + + verifyNever(mockResidentRunner.debugDumpRenderTree()); + }); + + testUsingContext('U - debugDumpRenderTree with service protocol', () async { + await terminalHandler.processTerminalInput('U'); + + verify(mockResidentRunner.debugDumpSemanticsTreeInInverseHitTestOrder()).called(1); + }); + + testUsingContext('U - debugDumpSemanticsTreeInTraversalOrder without service protocol', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); + await terminalHandler.processTerminalInput('U'); + + verifyNever(mockResidentRunner.debugDumpSemanticsTreeInInverseHitTestOrder()); + }); + + testUsingContext('w,W - debugDumpApp with service protocol', () async { + await terminalHandler.processTerminalInput('w'); + await terminalHandler.processTerminalInput('W'); + + verify(mockResidentRunner.debugDumpApp()).called(2); + }); + + testUsingContext('w,W - debugDumpApp without service protocol', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); + await terminalHandler.processTerminalInput('w'); + await terminalHandler.processTerminalInput('W'); + + verifyNever(mockResidentRunner.debugDumpApp()); + }); + + testUsingContext('z,Z - debugToggleDebugCheckElevationsEnabled with service protocol', () async { + await terminalHandler.processTerminalInput('z'); + await terminalHandler.processTerminalInput('Z'); + + verify(mockResidentRunner.debugToggleDebugCheckElevationsEnabled()).called(2); + }); + + testUsingContext('z,Z - debugToggleDebugCheckElevationsEnabled without service protocol', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); + await terminalHandler.processTerminalInput('z'); + await terminalHandler.processTerminalInput('Z'); + + // This should probably be disable when the service protocol is not enabled. + verify(mockResidentRunner.debugToggleDebugCheckElevationsEnabled()).called(2); + }); + }); +} + +class MockDevice extends Mock implements Device { + MockDevice() { + when(isSupported()).thenReturn(true); + } +} + +class MockResidentRunner extends Mock implements ResidentRunner {} + +class MockFlutterDevice extends Mock implements FlutterDevice {} + +class TestRunner extends ResidentRunner { + TestRunner(List devices) + : super(devices); + + bool hasHelpBeenPrinted = false; + String receivedCommand; + + @override + Future cleanupAfterSignal() async { } + + @override + Future cleanupAtFinish() async { } + + @override + void printHelp({ bool details }) { + hasHelpBeenPrinted = true; + } + + @override + Future run({ + Completer connectionInfoCompleter, + Completer appStartedCompleter, + String route, + bool shouldBuild = true, + }) async => null; + + @override + Future attach({ + Completer connectionInfoCompleter, + Completer appStartedCompleter, + }) async => null; +} diff --git a/packages/fuchsia_remote_debug_protocol/pubspec.yaml b/packages/fuchsia_remote_debug_protocol/pubspec.yaml index 1ea1fed8971..69e44469b42 100644 --- a/packages/fuchsia_remote_debug_protocol/pubspec.yaml +++ b/packages/fuchsia_remote_debug_protocol/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: json_rpc_2: 2.1.0 process: 3.0.9 - web_socket_channel: 1.0.13 + web_socket_channel: 1.0.14 flutter_test: sdk: flutter flutter_driver: @@ -44,4 +44,4 @@ dependencies: dev_dependencies: mockito: 4.1.0 -# PUBSPEC CHECKSUM: fc49 +# PUBSPEC CHECKSUM: fd4a