From 7b423cc224fe628680e93d95e8c78b6a56f0c8b3 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 10:12:00 -0800 Subject: [PATCH 01/27] test code --- packages/espresso/example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/espresso/example/lib/main.dart b/packages/espresso/example/lib/main.dart index c74423f507e8..72bfbe1f26d8 100644 --- a/packages/espresso/example/lib/main.dart +++ b/packages/espresso/example/lib/main.dart @@ -8,7 +8,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'Flutter Demo1', theme: ThemeData( // This is the theme of your application. // From fd9d52db3f7e0353f541754e0377fbb92523f05f Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 10:12:57 -0800 Subject: [PATCH 02/27] only run builds --- .cirrus.yml | 272 ++++++++++++++++++++++++++-------------------------- 1 file changed, 136 insertions(+), 136 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 4ec73ea3f24c..0075181ded78 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,110 +1,110 @@ -task: - # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins - only_if: $CIRRUS_TAG == '' - use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' && $CIRRUS_PR == '' - container: - dockerfile: .ci/Dockerfile - cpu: 8 - memory: 16G - env: - INTEGRATION_TEST_PATH: "./packages/integration_test" - upgrade_script: - - flutter channel stable - - flutter upgrade - - flutter channel master - - flutter upgrade - - git fetch origin master - activate_script: pub global activate flutter_plugin_tools - matrix: - - name: publishable - script: - - flutter channel master - - ./script/check_publish.sh - - name: format - install_script: - - wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - - - sudo apt-add-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-7 main" - - sudo apt-get update - - sudo apt-get install -y --allow-unauthenticated clang-format-7 - format_script: ./script/incremental_build.sh format --travis --clang-format=clang-format-7 - - name: test - env: - matrix: - CHANNEL: "master" - CHANNEL: "stable" - test_script: - # TODO(jackson): Allow web plugins once supported on stable - # https://github.com/flutter/flutter/issues/42864 - - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - - flutter channel $CHANNEL - - ./script/incremental_build.sh test - - name: analyze - env: - matrix: - CHANNEL: "master" - CHANNEL: "stable" - script: ./script/incremental_build.sh analyze - - name: build_all_plugins_apk - env: - matrix: - CHANNEL: "master" - CHANNEL: "stable" - script: - # TODO(jackson): Allow web plugins once supported on stable - # https://github.com/flutter/flutter/issues/42864 - - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - - flutter channel $CHANNEL - - ./script/build_all_plugins_app.sh apk - - name: integration_web_smoke_test - # Tests integration example test in web. - only_if: "changesInclude('.cirrus.yml', 'packages/integration_test/**') || $CIRRUS_PR == ''" - install_script: - - flutter config --enable-web - - git clone https://github.com/flutter/web_installers.git - - cd web_installers/packages/web_drivers/ - - pub get - - dart lib/web_driver_installer.dart chromedriver --install-only - - ./chromedriver/chromedriver --port=4444 & - test_script: - - cd $INTEGRATION_TEST_PATH/example/ - - flutter drive -v --driver=test_driver/integration_test.dart --target=integration_test/example_test.dart -d web-server --release --browser-name=chrome - - name: build-apks+java-test+firebase-test-lab - env: - matrix: - PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" - PLUGIN_SHARDING: "--shardIndex 1 --shardCount 4" - PLUGIN_SHARDING: "--shardIndex 2 --shardCount 4" - PLUGIN_SHARDING: "--shardIndex 3 --shardCount 4" - matrix: - CHANNEL: "master" - CHANNEL: "stable" - MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] - GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[07586610af1fdfc894e5969f70ef2458341b9b7e9c3b7c4225a663b4a48732b7208a4d91c3b7d45305a6b55fa2a37fc4] - script: - # TODO(jackson): Allow web plugins once supported on stable - # https://github.com/flutter/flutter/issues/42864 - - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - - flutter channel $CHANNEL - # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they - # might include non-ASCII characters which makes Gradle crash. - # See: https://github.com/flutter/flutter/issues/24935 - # This is a temporary workaround until we figure how to properly configure - # a UTF8 locale on Cirrus (or until the Gradle bug is fixed). - # TODO(amirh): Set the locale to UTF8. - - echo "$CIRRUS_CHANGE_MESSAGE" > /tmp/cirrus_change_message.txt - - echo "$CIRRUS_COMMIT_MESSAGE" > /tmp/cirrus_commit_message.txt - - export CIRRUS_CHANGE_MESSAGE="" - - export CIRRUS_COMMIT_MESSAGE="" - - ./script/incremental_build.sh build-examples --apk - - ./script/incremental_build.sh java-test # must come after apk build - - if [[ $GCLOUD_FIREBASE_TESTLAB_KEY == ENCRYPTED* ]]; then - - echo "This user does not have permission to run Firebase Test Lab tests." - - else - - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json - - ./script/incremental_build.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 - - fi - - export CIRRUS_CHANGE_MESSAGE=`cat /tmp/cirrus_change_message.txt` - - export CIRRUS_COMMIT_MESSAGE=`cat /tmp/cirrus_commit_message.txt` +# task: +# # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins +# only_if: $CIRRUS_TAG == '' +# use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' && $CIRRUS_PR == '' +# container: +# dockerfile: .ci/Dockerfile +# cpu: 8 +# memory: 16G +# env: +# INTEGRATION_TEST_PATH: "./packages/integration_test" +# upgrade_script: +# - flutter channel stable +# - flutter upgrade +# - flutter channel master +# - flutter upgrade +# - git fetch origin master +# activate_script: pub global activate flutter_plugin_tools +# matrix: +# - name: publishable +# script: +# - flutter channel master +# - ./script/check_publish.sh +# - name: format +# install_script: +# - wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - +# - sudo apt-add-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-7 main" +# - sudo apt-get update +# - sudo apt-get install -y --allow-unauthenticated clang-format-7 +# format_script: ./script/incremental_build.sh format --travis --clang-format=clang-format-7 +# - name: test +# env: +# matrix: +# CHANNEL: "master" +# CHANNEL: "stable" +# test_script: +# # TODO(jackson): Allow web plugins once supported on stable +# # https://github.com/flutter/flutter/issues/42864 +# - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi +# - flutter channel $CHANNEL +# - ./script/incremental_build.sh test +# - name: analyze +# env: +# matrix: +# CHANNEL: "master" +# CHANNEL: "stable" +# script: ./script/incremental_build.sh analyze +# - name: build_all_plugins_apk +# env: +# matrix: +# CHANNEL: "master" +# CHANNEL: "stable" +# script: +# # TODO(jackson): Allow web plugins once supported on stable +# # https://github.com/flutter/flutter/issues/42864 +# - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi +# - flutter channel $CHANNEL +# - ./script/build_all_plugins_app.sh apk +# - name: integration_web_smoke_test +# # Tests integration example test in web. +# only_if: "changesInclude('.cirrus.yml', 'packages/integration_test/**') || $CIRRUS_PR == ''" +# install_script: +# - flutter config --enable-web +# - git clone https://github.com/flutter/web_installers.git +# - cd web_installers/packages/web_drivers/ +# - pub get +# - dart lib/web_driver_installer.dart chromedriver --install-only +# - ./chromedriver/chromedriver --port=4444 & +# test_script: +# - cd $INTEGRATION_TEST_PATH/example/ +# - flutter drive -v --driver=test_driver/integration_test.dart --target=integration_test/example_test.dart -d web-server --release --browser-name=chrome +# - name: build-apks+java-test+firebase-test-lab +# env: +# matrix: +# PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" +# PLUGIN_SHARDING: "--shardIndex 1 --shardCount 4" +# PLUGIN_SHARDING: "--shardIndex 2 --shardCount 4" +# PLUGIN_SHARDING: "--shardIndex 3 --shardCount 4" +# matrix: +# CHANNEL: "master" +# CHANNEL: "stable" +# MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] +# GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[07586610af1fdfc894e5969f70ef2458341b9b7e9c3b7c4225a663b4a48732b7208a4d91c3b7d45305a6b55fa2a37fc4] +# script: +# # TODO(jackson): Allow web plugins once supported on stable +# # https://github.com/flutter/flutter/issues/42864 +# - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi +# - flutter channel $CHANNEL +# # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they +# # might include non-ASCII characters which makes Gradle crash. +# # See: https://github.com/flutter/flutter/issues/24935 +# # This is a temporary workaround until we figure how to properly configure +# # a UTF8 locale on Cirrus (or until the Gradle bug is fixed). +# # TODO(amirh): Set the locale to UTF8. +# - echo "$CIRRUS_CHANGE_MESSAGE" > /tmp/cirrus_change_message.txt +# - echo "$CIRRUS_COMMIT_MESSAGE" > /tmp/cirrus_commit_message.txt +# - export CIRRUS_CHANGE_MESSAGE="" +# - export CIRRUS_COMMIT_MESSAGE="" +# - ./script/incremental_build.sh build-examples --apk +# - ./script/incremental_build.sh java-test # must come after apk build +# - if [[ $GCLOUD_FIREBASE_TESTLAB_KEY == ENCRYPTED* ]]; then +# - echo "This user does not have permission to run Firebase Test Lab tests." +# - else +# - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json +# - ./script/incremental_build.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 +# - fi +# - export CIRRUS_CHANGE_MESSAGE=`cat /tmp/cirrus_change_message.txt` +# - export CIRRUS_COMMIT_MESSAGE=`cat /tmp/cirrus_commit_message.txt` task: # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins @@ -183,7 +183,7 @@ task: PLUGIN_SHARDING: "--shardIndex 2 --shardCount 4" PLUGIN_SHARDING: "--shardIndex 3 --shardCount 4" matrix: - CHANNEL: "master" + # CHANNEL: "master" CHANNEL: "stable" SIMCTL_CHILD_MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] build_script: @@ -192,31 +192,31 @@ task: - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - flutter channel $CHANNEL - ./script/incremental_build.sh build-examples --ipa - - ./script/incremental_build.sh drive-examples - - ./script/incremental_build.sh xctest --target RunnerUITests --skip $PLUGINS_TO_SKIP_XCTESTS -task: - # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins - only_if: $CIRRUS_TAG == '' - use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' - osx_instance: - image: catalina-xcode-11.3.1-flutter - setup_script: - - flutter config --enable-macos-desktop - upgrade_script: - - sudo gem install cocoapods - - flutter channel master - - flutter upgrade - - git fetch origin master - activate_script: pub global activate flutter_plugin_tools - matrix: - - name: build_all_plugins_app - script: - - flutter channel master - - ./script/build_all_plugins_app.sh macos - - name: build-apps+drive-examples - env: - PATH: $PATH:/usr/local/bin - build_script: - - flutter channel master - - ./script/incremental_build.sh build-examples --macos --no-ipa - - ./script/incremental_build.sh drive-examples --macos + # - ./script/incremental_build.sh drive-examples + # - ./script/incremental_build.sh xctest --target RunnerUITests --skip $PLUGINS_TO_SKIP_XCTESTS +# task: +# # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins +# only_if: $CIRRUS_TAG == '' +# use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' +# osx_instance: +# image: catalina-xcode-11.3.1-flutter +# setup_script: +# - flutter config --enable-macos-desktop +# upgrade_script: +# - sudo gem install cocoapods +# - flutter channel master +# - flutter upgrade +# - git fetch origin master +# activate_script: pub global activate flutter_plugin_tools +# matrix: +# - name: build_all_plugins_app +# script: +# - flutter channel master +# - ./script/build_all_plugins_app.sh macos +# - name: build-apps+drive-examples +# env: +# PATH: $PATH:/usr/local/bin +# build_script: +# - flutter channel master +# - ./script/incremental_build.sh build-examples --macos --no-ipa +# - ./script/incremental_build.sh drive-examples --macos From 1d929bf3e0705f3435cea7024955ab7c20af07bd Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 10:14:46 -0800 Subject: [PATCH 03/27] remove more tests --- .cirrus.yml | 96 ++++++++++++++++++++++++++--------------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 0075181ded78..d809f30dcd1a 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -106,32 +106,32 @@ # - export CIRRUS_CHANGE_MESSAGE=`cat /tmp/cirrus_change_message.txt` # - export CIRRUS_COMMIT_MESSAGE=`cat /tmp/cirrus_commit_message.txt` -task: - # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins - only_if: $CIRRUS_TAG == '' - use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' && $CIRRUS_PR == '' - container: - dockerfile: .ci/Dockerfile-LinuxDesktop - cpu: 8 - memory: 16G - env: - INTEGRATION_TEST_PATH: "./packages/integration_test" - upgrade_script: - - flutter channel stable - - flutter upgrade - - flutter channel master - - flutter upgrade - - git fetch origin master - activate_script: pub global activate flutter_plugin_tools - matrix: - - name: build-linux+drive-examples - install_script: - - flutter config --enable-linux-desktop - build_script: - # TODO(stuartmorgan): Include stable once Linux is supported on stable. - - flutter channel master - - ./script/incremental_build.sh build-examples --linux - - xvfb-run ./script/incremental_build.sh drive-examples --linux +# task: +# # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins +# only_if: $CIRRUS_TAG == '' +# use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' && $CIRRUS_PR == '' +# container: +# dockerfile: .ci/Dockerfile-LinuxDesktop +# cpu: 8 +# memory: 16G +# env: +# INTEGRATION_TEST_PATH: "./packages/integration_test" +# upgrade_script: +# - flutter channel stable +# - flutter upgrade +# - flutter channel master +# - flutter upgrade +# - git fetch origin master +# activate_script: pub global activate flutter_plugin_tools +# matrix: +# - name: build-linux+drive-examples +# install_script: +# - flutter config --enable-linux-desktop +# build_script: +# # TODO(stuartmorgan): Include stable once Linux is supported on stable. +# - flutter channel master +# - ./script/incremental_build.sh build-examples --linux +# - xvfb-run ./script/incremental_build.sh drive-examples --linux task: # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins @@ -151,28 +151,28 @@ task: - xcrun simctl list - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-3 | xargs xcrun simctl boot matrix: - - name: build_all_plugins_ipa - env: - matrix: - CHANNEL: "master" - CHANNEL: "stable" - script: - # TODO(jackson): Allow web plugins once supported on stable - # https://github.com/flutter/flutter/issues/42864 - - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - - flutter channel $CHANNEL - - ./script/build_all_plugins_app.sh ios --no-codesign - - name: lint_darwin_plugins - env: - matrix: - PLUGIN_SHARDING: "--shardIndex 0 --shardCount 2" - PLUGIN_SHARDING: "--shardIndex 1 --shardCount 2" - script: - # TODO(jmagman): Lint macOS podspecs but skip any that fail library validation. - - find . -name "*.podspec" | xargs grep -l "osx" | xargs rm - # Skip the dummy podspecs used to placate the tool. - - find . -name "*_web*.podspec" -o -name "*_mac*.podspec" | xargs rm - - ./script/incremental_build.sh podspecs + # - name: build_all_plugins_ipa + # env: + # matrix: + # CHANNEL: "master" + # CHANNEL: "stable" + # script: + # # TODO(jackson): Allow web plugins once supported on stable + # # https://github.com/flutter/flutter/issues/42864 + # - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi + # - flutter channel $CHANNEL + # - ./script/build_all_plugins_app.sh ios --no-codesign + # - name: lint_darwin_plugins + # env: + # matrix: + # PLUGIN_SHARDING: "--shardIndex 0 --shardCount 2" + # PLUGIN_SHARDING: "--shardIndex 1 --shardCount 2" + # script: + # # TODO(jmagman): Lint macOS podspecs but skip any that fail library validation. + # - find . -name "*.podspec" | xargs grep -l "osx" | xargs rm + # # Skip the dummy podspecs used to placate the tool. + # - find . -name "*_web*.podspec" -o -name "*_mac*.podspec" | xargs rm + # - ./script/incremental_build.sh podspecs - name: build-ipas+drive-examples env: PATH: $PATH:/usr/local/bin From 839faa7b6645a1837545d2919764a38b74d6efa1 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 10:23:51 -0800 Subject: [PATCH 04/27] run against master --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index d809f30dcd1a..c74b3ab357b0 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -183,7 +183,7 @@ task: PLUGIN_SHARDING: "--shardIndex 2 --shardCount 4" PLUGIN_SHARDING: "--shardIndex 3 --shardCount 4" matrix: - # CHANNEL: "master" + CHANNEL: "master" CHANNEL: "stable" SIMCTL_CHILD_MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] build_script: From dd883bb3185edc52de43e0ba9da496e2d7d794ce Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 11:02:37 -0800 Subject: [PATCH 05/27] aslo update camera --- packages/camera/camera/test/camera_image_stream_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/test/camera_image_stream_test.dart b/packages/camera/camera/test/camera_image_stream_test.dart index 57e3aeb36f3f..2d339c454144 100644 --- a/packages/camera/camera/test/camera_image_stream_test.dart +++ b/packages/camera/camera/test/camera_image_stream_test.dart @@ -13,7 +13,7 @@ void main() { CameraPlatform.instance = MockCameraPlatform(); }); - test('startImageStream() throws $CameraException when uninitialized', () { + test('startImageStream() throws $CameraException when uninitialized 1', () { CameraController cameraController = CameraController( CameraDescription( name: 'cam', From 4bd77aa7e60785cad377d8a622508daae0f85d48 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 12:51:03 -0800 Subject: [PATCH 06/27] enable other jobs --- .cirrus.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index c74b3ab357b0..dedda6b7e21a 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -192,8 +192,8 @@ task: - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - flutter channel $CHANNEL - ./script/incremental_build.sh build-examples --ipa - # - ./script/incremental_build.sh drive-examples - # - ./script/incremental_build.sh xctest --target RunnerUITests --skip $PLUGINS_TO_SKIP_XCTESTS + - ./script/incremental_build.sh drive-examples + - ./script/incremental_build.sh xctest --target RunnerUITests --skip $PLUGINS_TO_SKIP_XCTESTS # task: # # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins # only_if: $CIRRUS_TAG == '' From bd8781ab240088c8abf9a1e9f77e446df3bc69a2 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 13:01:18 -0800 Subject: [PATCH 07/27] enable all jobs --- .cirrus.yml | 362 ++++++++++++++++++++++++++-------------------------- 1 file changed, 181 insertions(+), 181 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index dedda6b7e21a..4ec73ea3f24c 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,137 +1,137 @@ -# task: -# # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins -# only_if: $CIRRUS_TAG == '' -# use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' && $CIRRUS_PR == '' -# container: -# dockerfile: .ci/Dockerfile -# cpu: 8 -# memory: 16G -# env: -# INTEGRATION_TEST_PATH: "./packages/integration_test" -# upgrade_script: -# - flutter channel stable -# - flutter upgrade -# - flutter channel master -# - flutter upgrade -# - git fetch origin master -# activate_script: pub global activate flutter_plugin_tools -# matrix: -# - name: publishable -# script: -# - flutter channel master -# - ./script/check_publish.sh -# - name: format -# install_script: -# - wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - -# - sudo apt-add-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-7 main" -# - sudo apt-get update -# - sudo apt-get install -y --allow-unauthenticated clang-format-7 -# format_script: ./script/incremental_build.sh format --travis --clang-format=clang-format-7 -# - name: test -# env: -# matrix: -# CHANNEL: "master" -# CHANNEL: "stable" -# test_script: -# # TODO(jackson): Allow web plugins once supported on stable -# # https://github.com/flutter/flutter/issues/42864 -# - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi -# - flutter channel $CHANNEL -# - ./script/incremental_build.sh test -# - name: analyze -# env: -# matrix: -# CHANNEL: "master" -# CHANNEL: "stable" -# script: ./script/incremental_build.sh analyze -# - name: build_all_plugins_apk -# env: -# matrix: -# CHANNEL: "master" -# CHANNEL: "stable" -# script: -# # TODO(jackson): Allow web plugins once supported on stable -# # https://github.com/flutter/flutter/issues/42864 -# - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi -# - flutter channel $CHANNEL -# - ./script/build_all_plugins_app.sh apk -# - name: integration_web_smoke_test -# # Tests integration example test in web. -# only_if: "changesInclude('.cirrus.yml', 'packages/integration_test/**') || $CIRRUS_PR == ''" -# install_script: -# - flutter config --enable-web -# - git clone https://github.com/flutter/web_installers.git -# - cd web_installers/packages/web_drivers/ -# - pub get -# - dart lib/web_driver_installer.dart chromedriver --install-only -# - ./chromedriver/chromedriver --port=4444 & -# test_script: -# - cd $INTEGRATION_TEST_PATH/example/ -# - flutter drive -v --driver=test_driver/integration_test.dart --target=integration_test/example_test.dart -d web-server --release --browser-name=chrome -# - name: build-apks+java-test+firebase-test-lab -# env: -# matrix: -# PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" -# PLUGIN_SHARDING: "--shardIndex 1 --shardCount 4" -# PLUGIN_SHARDING: "--shardIndex 2 --shardCount 4" -# PLUGIN_SHARDING: "--shardIndex 3 --shardCount 4" -# matrix: -# CHANNEL: "master" -# CHANNEL: "stable" -# MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] -# GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[07586610af1fdfc894e5969f70ef2458341b9b7e9c3b7c4225a663b4a48732b7208a4d91c3b7d45305a6b55fa2a37fc4] -# script: -# # TODO(jackson): Allow web plugins once supported on stable -# # https://github.com/flutter/flutter/issues/42864 -# - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi -# - flutter channel $CHANNEL -# # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they -# # might include non-ASCII characters which makes Gradle crash. -# # See: https://github.com/flutter/flutter/issues/24935 -# # This is a temporary workaround until we figure how to properly configure -# # a UTF8 locale on Cirrus (or until the Gradle bug is fixed). -# # TODO(amirh): Set the locale to UTF8. -# - echo "$CIRRUS_CHANGE_MESSAGE" > /tmp/cirrus_change_message.txt -# - echo "$CIRRUS_COMMIT_MESSAGE" > /tmp/cirrus_commit_message.txt -# - export CIRRUS_CHANGE_MESSAGE="" -# - export CIRRUS_COMMIT_MESSAGE="" -# - ./script/incremental_build.sh build-examples --apk -# - ./script/incremental_build.sh java-test # must come after apk build -# - if [[ $GCLOUD_FIREBASE_TESTLAB_KEY == ENCRYPTED* ]]; then -# - echo "This user does not have permission to run Firebase Test Lab tests." -# - else -# - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json -# - ./script/incremental_build.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 -# - fi -# - export CIRRUS_CHANGE_MESSAGE=`cat /tmp/cirrus_change_message.txt` -# - export CIRRUS_COMMIT_MESSAGE=`cat /tmp/cirrus_commit_message.txt` +task: + # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins + only_if: $CIRRUS_TAG == '' + use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' && $CIRRUS_PR == '' + container: + dockerfile: .ci/Dockerfile + cpu: 8 + memory: 16G + env: + INTEGRATION_TEST_PATH: "./packages/integration_test" + upgrade_script: + - flutter channel stable + - flutter upgrade + - flutter channel master + - flutter upgrade + - git fetch origin master + activate_script: pub global activate flutter_plugin_tools + matrix: + - name: publishable + script: + - flutter channel master + - ./script/check_publish.sh + - name: format + install_script: + - wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - + - sudo apt-add-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-7 main" + - sudo apt-get update + - sudo apt-get install -y --allow-unauthenticated clang-format-7 + format_script: ./script/incremental_build.sh format --travis --clang-format=clang-format-7 + - name: test + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" + test_script: + # TODO(jackson): Allow web plugins once supported on stable + # https://github.com/flutter/flutter/issues/42864 + - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi + - flutter channel $CHANNEL + - ./script/incremental_build.sh test + - name: analyze + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" + script: ./script/incremental_build.sh analyze + - name: build_all_plugins_apk + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" + script: + # TODO(jackson): Allow web plugins once supported on stable + # https://github.com/flutter/flutter/issues/42864 + - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi + - flutter channel $CHANNEL + - ./script/build_all_plugins_app.sh apk + - name: integration_web_smoke_test + # Tests integration example test in web. + only_if: "changesInclude('.cirrus.yml', 'packages/integration_test/**') || $CIRRUS_PR == ''" + install_script: + - flutter config --enable-web + - git clone https://github.com/flutter/web_installers.git + - cd web_installers/packages/web_drivers/ + - pub get + - dart lib/web_driver_installer.dart chromedriver --install-only + - ./chromedriver/chromedriver --port=4444 & + test_script: + - cd $INTEGRATION_TEST_PATH/example/ + - flutter drive -v --driver=test_driver/integration_test.dart --target=integration_test/example_test.dart -d web-server --release --browser-name=chrome + - name: build-apks+java-test+firebase-test-lab + env: + matrix: + PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" + PLUGIN_SHARDING: "--shardIndex 1 --shardCount 4" + PLUGIN_SHARDING: "--shardIndex 2 --shardCount 4" + PLUGIN_SHARDING: "--shardIndex 3 --shardCount 4" + matrix: + CHANNEL: "master" + CHANNEL: "stable" + MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] + GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[07586610af1fdfc894e5969f70ef2458341b9b7e9c3b7c4225a663b4a48732b7208a4d91c3b7d45305a6b55fa2a37fc4] + script: + # TODO(jackson): Allow web plugins once supported on stable + # https://github.com/flutter/flutter/issues/42864 + - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi + - flutter channel $CHANNEL + # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they + # might include non-ASCII characters which makes Gradle crash. + # See: https://github.com/flutter/flutter/issues/24935 + # This is a temporary workaround until we figure how to properly configure + # a UTF8 locale on Cirrus (or until the Gradle bug is fixed). + # TODO(amirh): Set the locale to UTF8. + - echo "$CIRRUS_CHANGE_MESSAGE" > /tmp/cirrus_change_message.txt + - echo "$CIRRUS_COMMIT_MESSAGE" > /tmp/cirrus_commit_message.txt + - export CIRRUS_CHANGE_MESSAGE="" + - export CIRRUS_COMMIT_MESSAGE="" + - ./script/incremental_build.sh build-examples --apk + - ./script/incremental_build.sh java-test # must come after apk build + - if [[ $GCLOUD_FIREBASE_TESTLAB_KEY == ENCRYPTED* ]]; then + - echo "This user does not have permission to run Firebase Test Lab tests." + - else + - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json + - ./script/incremental_build.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 + - fi + - export CIRRUS_CHANGE_MESSAGE=`cat /tmp/cirrus_change_message.txt` + - export CIRRUS_COMMIT_MESSAGE=`cat /tmp/cirrus_commit_message.txt` -# task: -# # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins -# only_if: $CIRRUS_TAG == '' -# use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' && $CIRRUS_PR == '' -# container: -# dockerfile: .ci/Dockerfile-LinuxDesktop -# cpu: 8 -# memory: 16G -# env: -# INTEGRATION_TEST_PATH: "./packages/integration_test" -# upgrade_script: -# - flutter channel stable -# - flutter upgrade -# - flutter channel master -# - flutter upgrade -# - git fetch origin master -# activate_script: pub global activate flutter_plugin_tools -# matrix: -# - name: build-linux+drive-examples -# install_script: -# - flutter config --enable-linux-desktop -# build_script: -# # TODO(stuartmorgan): Include stable once Linux is supported on stable. -# - flutter channel master -# - ./script/incremental_build.sh build-examples --linux -# - xvfb-run ./script/incremental_build.sh drive-examples --linux +task: + # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins + only_if: $CIRRUS_TAG == '' + use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' && $CIRRUS_PR == '' + container: + dockerfile: .ci/Dockerfile-LinuxDesktop + cpu: 8 + memory: 16G + env: + INTEGRATION_TEST_PATH: "./packages/integration_test" + upgrade_script: + - flutter channel stable + - flutter upgrade + - flutter channel master + - flutter upgrade + - git fetch origin master + activate_script: pub global activate flutter_plugin_tools + matrix: + - name: build-linux+drive-examples + install_script: + - flutter config --enable-linux-desktop + build_script: + # TODO(stuartmorgan): Include stable once Linux is supported on stable. + - flutter channel master + - ./script/incremental_build.sh build-examples --linux + - xvfb-run ./script/incremental_build.sh drive-examples --linux task: # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins @@ -151,28 +151,28 @@ task: - xcrun simctl list - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-3 | xargs xcrun simctl boot matrix: - # - name: build_all_plugins_ipa - # env: - # matrix: - # CHANNEL: "master" - # CHANNEL: "stable" - # script: - # # TODO(jackson): Allow web plugins once supported on stable - # # https://github.com/flutter/flutter/issues/42864 - # - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - # - flutter channel $CHANNEL - # - ./script/build_all_plugins_app.sh ios --no-codesign - # - name: lint_darwin_plugins - # env: - # matrix: - # PLUGIN_SHARDING: "--shardIndex 0 --shardCount 2" - # PLUGIN_SHARDING: "--shardIndex 1 --shardCount 2" - # script: - # # TODO(jmagman): Lint macOS podspecs but skip any that fail library validation. - # - find . -name "*.podspec" | xargs grep -l "osx" | xargs rm - # # Skip the dummy podspecs used to placate the tool. - # - find . -name "*_web*.podspec" -o -name "*_mac*.podspec" | xargs rm - # - ./script/incremental_build.sh podspecs + - name: build_all_plugins_ipa + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" + script: + # TODO(jackson): Allow web plugins once supported on stable + # https://github.com/flutter/flutter/issues/42864 + - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi + - flutter channel $CHANNEL + - ./script/build_all_plugins_app.sh ios --no-codesign + - name: lint_darwin_plugins + env: + matrix: + PLUGIN_SHARDING: "--shardIndex 0 --shardCount 2" + PLUGIN_SHARDING: "--shardIndex 1 --shardCount 2" + script: + # TODO(jmagman): Lint macOS podspecs but skip any that fail library validation. + - find . -name "*.podspec" | xargs grep -l "osx" | xargs rm + # Skip the dummy podspecs used to placate the tool. + - find . -name "*_web*.podspec" -o -name "*_mac*.podspec" | xargs rm + - ./script/incremental_build.sh podspecs - name: build-ipas+drive-examples env: PATH: $PATH:/usr/local/bin @@ -194,29 +194,29 @@ task: - ./script/incremental_build.sh build-examples --ipa - ./script/incremental_build.sh drive-examples - ./script/incremental_build.sh xctest --target RunnerUITests --skip $PLUGINS_TO_SKIP_XCTESTS -# task: -# # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins -# only_if: $CIRRUS_TAG == '' -# use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' -# osx_instance: -# image: catalina-xcode-11.3.1-flutter -# setup_script: -# - flutter config --enable-macos-desktop -# upgrade_script: -# - sudo gem install cocoapods -# - flutter channel master -# - flutter upgrade -# - git fetch origin master -# activate_script: pub global activate flutter_plugin_tools -# matrix: -# - name: build_all_plugins_app -# script: -# - flutter channel master -# - ./script/build_all_plugins_app.sh macos -# - name: build-apps+drive-examples -# env: -# PATH: $PATH:/usr/local/bin -# build_script: -# - flutter channel master -# - ./script/incremental_build.sh build-examples --macos --no-ipa -# - ./script/incremental_build.sh drive-examples --macos +task: + # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins + only_if: $CIRRUS_TAG == '' + use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' + osx_instance: + image: catalina-xcode-11.3.1-flutter + setup_script: + - flutter config --enable-macos-desktop + upgrade_script: + - sudo gem install cocoapods + - flutter channel master + - flutter upgrade + - git fetch origin master + activate_script: pub global activate flutter_plugin_tools + matrix: + - name: build_all_plugins_app + script: + - flutter channel master + - ./script/build_all_plugins_app.sh macos + - name: build-apps+drive-examples + env: + PATH: $PATH:/usr/local/bin + build_script: + - flutter channel master + - ./script/incremental_build.sh build-examples --macos --no-ipa + - ./script/incremental_build.sh drive-examples --macos From 64fa7602f09e7e14f97225c12a422c94429e9c18 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 13:53:28 -0800 Subject: [PATCH 08/27] run for all packages --- script/incremental_build.sh | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/script/incremental_build.sh b/script/incremental_build.sh index 3911f0a6e9c8..4b814a006e4e 100755 --- a/script/incremental_build.sh +++ b/script/incremental_build.sh @@ -52,17 +52,18 @@ if [[ "${BRANCH_NAME}" == "master" ]]; then (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) else # Sets CHANGED_PACKAGES - check_changed_packages + # check_changed_packages + (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) - if [[ "$CHANGED_PACKAGES" == "" ]]; then - echo "No changes detected in packages." - echo "Running for all packages" - (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) - else - echo running "${ACTIONS[@]}" - (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --plugins="$CHANGED_PACKAGES" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) - echo "Running version check for changed packages" - # TODO(egarciad): Enable this check once in master. - # (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools version-check --base_sha="$(get_branch_base_sha)") - fi + # if [[ "$CHANGED_PACKAGES" == "" ]]; then + # echo "No changes detected in packages." + # echo "Running for all packages" + # (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) + # else + # echo running "${ACTIONS[@]}" + # (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --plugins="$CHANGED_PACKAGES" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) + # echo "Running version check for changed packages" + # # TODO(egarciad): Enable this check once in master. + # # (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools version-check --base_sha="$(get_branch_base_sha)") + # fi fi From 5a1f13514e03e0a6e046f8f0d7a43218ffd99c47 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 15:22:33 -0800 Subject: [PATCH 09/27] add git clean --- .cirrus.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.cirrus.yml b/.cirrus.yml index 4ec73ea3f24c..e0e0886baef2 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -191,6 +191,7 @@ task: # https://github.com/flutter/flutter/issues/42864 - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - flutter channel $CHANNEL + - git clean -dfxx - ./script/incremental_build.sh build-examples --ipa - ./script/incremental_build.sh drive-examples - ./script/incremental_build.sh xctest --target RunnerUITests --skip $PLUGINS_TO_SKIP_XCTESTS From 3d891cb2a5f2642f3f760272be3f9992a0f2660a Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 15:40:43 -0800 Subject: [PATCH 10/27] use local plugin_tools --- .cirrus.yml | 2 +- plugin_tools | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 160000 plugin_tools diff --git a/.cirrus.yml b/.cirrus.yml index e0e0886baef2..bb5bd5fac53f 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -146,7 +146,7 @@ task: - flutter channel master - flutter upgrade - git fetch origin master - activate_script: pub global activate flutter_plugin_tools + activate_script: pub global activate -s path ./plugin_tools create_simulator_script: - xcrun simctl list - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-3 | xargs xcrun simctl boot diff --git a/plugin_tools b/plugin_tools new file mode 160000 index 000000000000..ce5cae0d800a --- /dev/null +++ b/plugin_tools @@ -0,0 +1 @@ +Subproject commit ce5cae0d800aa9100e902fc0a5103ef350db0c1d From 2f819d548c4d3ecb4d80f2759134f359b2e27791 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 15:47:48 -0800 Subject: [PATCH 11/27] add plugin tools --- .cirrus.yml | 2 +- packages/plugin_tools/CHANGELOG.md | 288 ++++++++++ packages/plugin_tools/LICENSE | 29 + packages/plugin_tools/README.md | 45 ++ packages/plugin_tools/analysis_options.yaml | 1 + .../bin/flutter_plugin_tools.dart | 5 + .../plugin_tools/lib/src/analyze_command.dart | 94 ++++ .../lib/src/build_examples_command.dart | 191 +++++++ packages/plugin_tools/lib/src/common.dart | 466 ++++++++++++++++ .../src/create_all_plugins_app_command.dart | 200 +++++++ .../lib/src/drive_examples_command.dart | 210 ++++++++ .../lib/src/firebase_test_lab_command.dart | 264 +++++++++ .../plugin_tools/lib/src/format_command.dart | 147 +++++ .../lib/src/java_test_command.dart | 89 +++ .../lib/src/lint_podspecs_command.dart | 146 +++++ .../plugin_tools/lib/src/list_command.dart | 60 +++ packages/plugin_tools/lib/src/main.dart | 63 +++ .../lib/src/publish_plugin_command.dart | 227 ++++++++ .../plugin_tools/lib/src/test_command.dart | 101 ++++ .../lib/src/version_check_command.dart | 220 ++++++++ .../plugin_tools/lib/src/xctest_command.dart | 216 ++++++++ packages/plugin_tools/pubspec.yaml | 30 ++ .../test/analyze_command_test.dart | 93 ++++ .../test/build_examples_command_test.dart | 470 ++++++++++++++++ packages/plugin_tools/test/common_test.dart | 100 ++++ .../test/drive_examples_command_test.dart | 505 ++++++++++++++++++ .../test/firebase_test_lab_test.dart | 256 +++++++++ .../test/lint_podspecs_command_test.dart | 202 +++++++ .../plugin_tools/test/list_command_test.dart | 198 +++++++ packages/plugin_tools/test/mocks.dart | 33 ++ .../test/publish_plugin_command_test.dart | 359 +++++++++++++ .../plugin_tools/test/test_command_test.dart | 154 ++++++ packages/plugin_tools/test/util.dart | 291 ++++++++++ .../plugin_tools/test/version_check_test.dart | 319 +++++++++++ .../test/xctest_command_test.dart | 358 +++++++++++++ plugin_tools | 1 - 36 files changed, 6431 insertions(+), 2 deletions(-) create mode 100644 packages/plugin_tools/CHANGELOG.md create mode 100644 packages/plugin_tools/LICENSE create mode 100644 packages/plugin_tools/README.md create mode 100644 packages/plugin_tools/analysis_options.yaml create mode 100644 packages/plugin_tools/bin/flutter_plugin_tools.dart create mode 100644 packages/plugin_tools/lib/src/analyze_command.dart create mode 100644 packages/plugin_tools/lib/src/build_examples_command.dart create mode 100644 packages/plugin_tools/lib/src/common.dart create mode 100644 packages/plugin_tools/lib/src/create_all_plugins_app_command.dart create mode 100644 packages/plugin_tools/lib/src/drive_examples_command.dart create mode 100644 packages/plugin_tools/lib/src/firebase_test_lab_command.dart create mode 100644 packages/plugin_tools/lib/src/format_command.dart create mode 100644 packages/plugin_tools/lib/src/java_test_command.dart create mode 100644 packages/plugin_tools/lib/src/lint_podspecs_command.dart create mode 100644 packages/plugin_tools/lib/src/list_command.dart create mode 100644 packages/plugin_tools/lib/src/main.dart create mode 100644 packages/plugin_tools/lib/src/publish_plugin_command.dart create mode 100644 packages/plugin_tools/lib/src/test_command.dart create mode 100644 packages/plugin_tools/lib/src/version_check_command.dart create mode 100644 packages/plugin_tools/lib/src/xctest_command.dart create mode 100644 packages/plugin_tools/pubspec.yaml create mode 100644 packages/plugin_tools/test/analyze_command_test.dart create mode 100644 packages/plugin_tools/test/build_examples_command_test.dart create mode 100644 packages/plugin_tools/test/common_test.dart create mode 100644 packages/plugin_tools/test/drive_examples_command_test.dart create mode 100644 packages/plugin_tools/test/firebase_test_lab_test.dart create mode 100644 packages/plugin_tools/test/lint_podspecs_command_test.dart create mode 100644 packages/plugin_tools/test/list_command_test.dart create mode 100644 packages/plugin_tools/test/mocks.dart create mode 100644 packages/plugin_tools/test/publish_plugin_command_test.dart create mode 100644 packages/plugin_tools/test/test_command_test.dart create mode 100644 packages/plugin_tools/test/util.dart create mode 100644 packages/plugin_tools/test/version_check_test.dart create mode 100644 packages/plugin_tools/test/xctest_command_test.dart delete mode 160000 plugin_tools diff --git a/.cirrus.yml b/.cirrus.yml index bb5bd5fac53f..6dafca5fb435 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -146,7 +146,7 @@ task: - flutter channel master - flutter upgrade - git fetch origin master - activate_script: pub global activate -s path ./plugin_tools + activate_script: pub global activate -s path ./package/plugin_tools create_simulator_script: - xcrun simctl list - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-3 | xargs xcrun simctl boot diff --git a/packages/plugin_tools/CHANGELOG.md b/packages/plugin_tools/CHANGELOG.md new file mode 100644 index 000000000000..25dce424eee4 --- /dev/null +++ b/packages/plugin_tools/CHANGELOG.md @@ -0,0 +1,288 @@ +## v.0.0.45+1 + +- Don't call `flutter format` if there are no Dart files to format. + +## v.0.0.45 + +- Add exclude flag to exclude any plugin from further processing. + +## v.0.0.44+7 + +- `all-plugins-app` doesn't override the AGP version. + +## v.0.0.44+6 + +- Fix code formatting. + +## v.0.0.44+5 + +- Remove `-v` flag on drive-examples. + +## v.0.0.44+4 + +- Fix bug where directory isn't passed + +## v.0.0.44+3 + +- More verbose logging + +## v.0.0.44+2 + +- Remove pre-alpha Windows workaround to create examples on the fly. + +## v.0.0.44+1 + +- Print packages that passed tests in `xctest` command. +- Remove printing the whole list of simulators. + +## v.0.0.44 + +- Add 'xctest' command to run xctests. + +## v.0.0.43 + +- Allow minor `*-nullsafety` pre release packages. + +## v.0.0.42+1 + +- Fix test command when `--enable-experiment` is called. + +## v.0.0.42 + +- Allow `*-nullsafety` pre release packages. + +## v.0.0.41 + +- Support `--enable-experiment` flag in subcommands `test`, `build-examples`, `drive-examples`, +and `firebase-test-lab`. + +## v.0.0.40 + +- Support `integration_test/` directory for `drive-examples` command + +## v.0.0.39 + +- Support `integration_test/` directory for `package:integration_test` + +## v.0.0.38 + +- Add C++ and ObjC++ to clang-format. + +## v.0.0.37+2 + +- Make `http` and `http_multi_server` dependency version constraint more flexible. + +## v.0.0.37+1 + +- All_plugin test puts the plugin dependencies into dependency_overrides. + +## v.0.0.37 + +- Only builds mobile example apps when necessary. + +## v.0.0.36+3 + +- Add support for Linux plugins. + +## v.0.0.36+2 + +- Default to showing podspec lint warnings + +## v.0.0.36+1 + +- Serialize linting podspecs. + +## v.0.0.36 + +- Remove retry on Firebase Test Lab's call to gcloud set. +- Remove quiet flag from Firebase Test Lab's gcloud set command. +- Allow Firebase Test Lab command to continue past gcloud set network failures. + This is a mitigation for the network service sometimes not responding, + but it isn't actually necessary to have a network connection for this command. + +## v.0.0.35+1 + +- Minor cleanup to the analyze test. + +## v.0.0.35 + +- Firebase Test Lab command generates a configurable unique path suffix for results. + +## v.0.0.34 + +- Firebase Test Lab command now only tries to configure the project once +- Firebase Test Lab command now retries project configuration up to five times. + +## v.0.0.33+1 + +- Fixes formatting issues that got past our CI due to + https://github.com/flutter/flutter/issues/51585. +- Changes the default package name for testing method `createFakePubspec` back + its previous behavior. + +## v.0.0.33 + +- Version check command now fails on breaking changes to platform interfaces. +- Updated version check test to be more flexible. + +## v.0.0.32+7 + +- Ensure that Firebase Test Lab tests have a unique storage bucket for each test run. + +## v.0.0.32+6 + +- Ensure that Firebase Test Lab tests have a unique storage bucket for each package. + +## v.0.0.32+5 + +- Remove --fail-fast and --silent from lint podspec command. + +## v.0.0.32+4 + +- Update `publish-plugin` to use `flutter pub publish` instead of just `pub + publish`. Enforces a `pub publish` command that matches the Dart SDK in the + user's Flutter install. + +## v.0.0.32+3 + +- Update Firebase Testlab deprecated test device. (Pixel 3 API 28 -> Pixel 4 API 29). + +## v.0.0.32+2 + +- Runs pub get before building macos to avoid failures. + +## v.0.0.32+1 + +- Default macOS example builds to false. Previously they were running whenever + CI was itself running on macOS. + +## v.0.0.32 + +- `analyze` now asserts that the global `analysis_options.yaml` is the only one + by default. Individual directories can be excluded from this check with the + new `--custom-analysis` flag. + +## v.0.0.31+1 + +- Add --skip and --no-analyze flags to podspec command. + +## v.0.0.31 + +- Add support for macos on `DriveExamplesCommand` and `BuildExamplesCommand`. + +## v.0.0.30 + +- Adopt pedantic analysis options, fix firebase_test_lab_test. + +## v.0.0.29 + +- Add a command to run pod lib lint on podspec files. + +## v.0.0.28 + +- Increase Firebase test lab timeouts to 5 minutes. + +## v.0.0.27 + +- Run tests with `--platform=chrome` for web plugins. + +## v.0.0.26 + +- Add a command for publishing plugins to pub. + +## v.0.0.25 + +- Update `DriveExamplesCommand` to use `ProcessRunner`. +- Make `DriveExamplesCommand` rely on `ProcessRunner` to determine if the test fails or not. +- Add simple tests for `DriveExamplesCommand`. + +## v.0.0.24 + +- Gracefully handle pubspec.yaml files for new plugins. +- Additional unit testing. + +## v.0.0.23 + +- Add a test case for transitive dependency solving in the + `create_all_plugins_app` command. + +## v.0.0.22 + +- Updated firebase-test-lab command with updated conventions for test locations. +- Updated firebase-test-lab to add an optional "device" argument. +- Updated version-check command to always compare refs instead of using the working copy. +- Added unit tests for the firebase-test-lab and version-check commands. +- Add ProcessRunner to mock running processes for testing. + +## v.0.0.21 + +- Support the `--plugins` argument for federated plugins. + +## v.0.0.20 + +- Support for finding federated plugins, where one directory contains + multiple packages for different platform implementations. + +## v.0.0.19+3 + +- Use `package:file` for file I/O. + +## v.0.0.19+2 + +- Use java as language when calling `flutter create`. + +## v.0.0.19+1 + +- Rename command for `CreateAllPluginsAppCommand`. + +## v.0.0.19 + +- Use flutter create to build app testing plugin compilation. + +## v.0.0.18+2 + +- Fix `.travis.yml` file name in `README.md`. + +## v0.0.18+1 + +- Skip version check if it contains `publish_to: none`. + +## v0.0.18 + +- Add option to exclude packages from generated pubspec command. + +## v0.0.17+4 + +- Avoid trying to version-check pubspecs that are missing a version. + +## v0.0.17+3 + +- version-check accounts for [pre-1.0 patch versions](https://github.com/flutter/flutter/issues/35412). + +## v0.0.17+2 + +- Fix exception handling for version checker + +## v0.0.17+1 + +- Fix bug where we used a flag instead of an option + +## v0.0.17 + +- Add a command for checking the version number + +## v0.0.16 + +- Add a command for generating `pubspec.yaml` for All Plugins app. + +## v0.0.15 + +- Add a command for running driver tests of plugin examples. + +## v0.0.14 + +- Check for dependencies->flutter instead of top level flutter node. + +## v0.0.13 + +- Differentiate between Flutter and non-Flutter (but potentially Flutter consumed) Dart packages. diff --git a/packages/plugin_tools/LICENSE b/packages/plugin_tools/LICENSE new file mode 100644 index 000000000000..5f015bc7b321 --- /dev/null +++ b/packages/plugin_tools/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2017, Flutter +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/plugin_tools/README.md b/packages/plugin_tools/README.md new file mode 100644 index 000000000000..efdc668e81a0 --- /dev/null +++ b/packages/plugin_tools/README.md @@ -0,0 +1,45 @@ +# Flutter Plugin Tools + +[![Build Status](https://travis-ci.org/flutter/plugin_tools.svg?branch=master)](https://travis-ci.org/flutter/plugin_tools) +[![pub package](https://img.shields.io/pub/v/flutter_plugin_tools.svg)](https://pub.dartlang.org/packages/flutter_plugin_tools) + + +Flutter Plugin Tools implements a CLI with various productivity tools for hosting multiple Flutter plugins in one github +repository. It is mainly used by the [flutter/plugins](https://github.com/flutter/plugins) and +[flutter/flutterfire](https://github.com/flutter/flutterfire) repositories. It was mainly written to facilitate +testing on Travis for these repositories (see [.travis.yml](https://github.com/flutter/plugins/blob/master/.travis.yml)). + +As an example, Flutter Plugin Tools allows you to: + +* Build all plugin example apps with one command +* Run the tests of all plugins with one command +* Format all Dart, Java, Objective-C, and C++ code in the repository +* Define shards of the above tasks + +## Installation + +In order to use the tools you need to enable them once by running the following command: + +```shell +$ pub global activate flutter_plugin_tools +``` + +## Requirements + +To use all features of `flutter_plugin_tools` you'll need the following commands in your `PATH`: +* `flutter` +* `git` +* `pub` (recommended: version from `/bin/cache/dart-sdk/bin`) +* `clang-format` version 5 (alternatively, you can provide the path via `--clang-format=`) +* [`pod`](https://guides.cocoapods.org/using/getting-started.html#installation) (macOS only) + +## Usage + +```shell +$ pub global run flutter_plugin_tools +$ pub global run flutter_plugin_tools --shardIndex 0 --shardCount 3 +``` + +Run commands from the `flutter/plugins` directory. Replace `` with `help` to print a list of available commands. +The sharded example above divides the plugins into three shards +and executes the tool on the first shard (index 0). diff --git a/packages/plugin_tools/analysis_options.yaml b/packages/plugin_tools/analysis_options.yaml new file mode 100644 index 000000000000..84a5e26f95de --- /dev/null +++ b/packages/plugin_tools/analysis_options.yaml @@ -0,0 +1 @@ +include: package:pedantic/analysis_options.1.8.0.yaml diff --git a/packages/plugin_tools/bin/flutter_plugin_tools.dart b/packages/plugin_tools/bin/flutter_plugin_tools.dart new file mode 100644 index 000000000000..43edcbfe2c68 --- /dev/null +++ b/packages/plugin_tools/bin/flutter_plugin_tools.dart @@ -0,0 +1,5 @@ +// 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. + +export 'package:flutter_plugin_tools/src/main.dart'; diff --git a/packages/plugin_tools/lib/src/analyze_command.dart b/packages/plugin_tools/lib/src/analyze_command.dart new file mode 100644 index 000000000000..8cd57fa0b338 --- /dev/null +++ b/packages/plugin_tools/lib/src/analyze_command.dart @@ -0,0 +1,94 @@ +// 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:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class AnalyzeCommand extends PluginCommand { + AnalyzeCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addMultiOption(_customAnalysisFlag, + help: + 'Directories (comma seperated) that are allowed to have their own analysis options.', + defaultsTo: []); + } + + static const String _customAnalysisFlag = 'custom-analysis'; + + @override + final String name = 'analyze'; + + @override + final String description = 'Analyzes all packages using package:tuneup.\n\n' + 'This command requires "pub" and "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + + print('Verifying analysis settings...'); + final List files = packagesDir.listSync(recursive: true); + for (final FileSystemEntity file in files) { + if (file.basename != 'analysis_options.yaml' && + file.basename != '.analysis_options') { + continue; + } + + final bool whitelisted = argResults[_customAnalysisFlag].any( + (String directory) => + p.isWithin(p.join(packagesDir.path, directory), file.path)); + if (whitelisted) { + continue; + } + + print('Found an extra analysis_options.yaml in ${file.absolute.path}.'); + print( + 'If this was deliberate, pass the package to the analyze command with the --$_customAnalysisFlag flag and try again.'); + throw ToolExit(1); + } + + print('Activating tuneup package...'); + await processRunner.runAndStream( + 'pub', ['global', 'activate', 'tuneup'], + workingDir: packagesDir, exitOnError: true); + + await for (Directory package in getPackages()) { + if (isFlutterPackage(package, fileSystem)) { + await processRunner.runAndStream('flutter', ['packages', 'get'], + workingDir: package, exitOnError: true); + } else { + await processRunner.runAndStream('pub', ['get'], + workingDir: package, exitOnError: true); + } + } + + final List failingPackages = []; + await for (Directory package in getPlugins()) { + final int exitCode = await processRunner.runAndStream( + 'pub', ['global', 'run', 'tuneup', 'check'], + workingDir: package); + if (exitCode != 0) { + failingPackages.add(p.basename(package.path)); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print('The following packages have analyzer errors (see above):'); + failingPackages.forEach((String package) { + print(' * $package'); + }); + throw ToolExit(1); + } + + print('No analyzer errors found!'); + } +} diff --git a/packages/plugin_tools/lib/src/build_examples_command.dart b/packages/plugin_tools/lib/src/build_examples_command.dart new file mode 100644 index 000000000000..1493b3cce05d --- /dev/null +++ b/packages/plugin_tools/lib/src/build_examples_command.dart @@ -0,0 +1,191 @@ +// 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 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'common.dart'; + +class BuildExamplesCommand extends PluginCommand { + BuildExamplesCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag(kLinux, defaultsTo: false); + argParser.addFlag(kMacos, defaultsTo: false); + argParser.addFlag(kWindows, defaultsTo: false); + argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS); + argParser.addFlag(kApk); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Enables the given Dart SDK experiments.', + ); + } + + @override + final String name = 'build-examples'; + + @override + final String description = + 'Builds all example apps (IPA for iOS and APK for Android).\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + if (!argResults[kIpa] && + !argResults[kApk] && + !argResults[kLinux] && + !argResults[kMacos] && + !argResults[kWindows]) { + print( + 'None of --linux, --macos, --windows, --apk nor --ipa were specified, ' + 'so not building anything.'); + return; + } + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + + final String enableExperiment = argResults[kEnableExperiment]; + + checkSharding(); + final List failingPackages = []; + await for (Directory plugin in getPlugins()) { + for (Directory example in getExamplesForPlugin(plugin)) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + + if (argResults[kLinux]) { + print('\nBUILDING Linux for $packageName'); + if (isLinuxPlugin(plugin, fileSystem)) { + int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kLinux, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (linux)'); + } + } else { + print('Linux is not supported by this plugin'); + } + } + + if (argResults[kMacos]) { + print('\nBUILDING macOS for $packageName'); + if (isMacOsPlugin(plugin, fileSystem)) { + // TODO(https://github.com/flutter/flutter/issues/46236): + // Builing macos without running flutter pub get first results + // in an error. + int exitCode = await processRunner.runAndStream( + flutterCommand, ['pub', 'get'], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (macos)'); + } else { + exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kMacos, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (macos)'); + } + } + } else { + print('macOS is not supported by this plugin'); + } + } + + if (argResults[kWindows]) { + print('\nBUILDING Windows for $packageName'); + if (isWindowsPlugin(plugin, fileSystem)) { + int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kWindows, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (windows)'); + } + } else { + print('Windows is not supported by this plugin'); + } + } + + if (argResults[kIpa]) { + print('\nBUILDING IPA for $packageName'); + if (isIosPlugin(plugin, fileSystem)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + '--verbose', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example, + exitOnError: true); + if (exitCode != 0) { + failingPackages.add('$packageName (ipa)'); + } + } else { + print('iOS is not supported by this plugin'); + } + } + print('end ios build'); + + if (argResults[kApk]) { + print('\nBUILDING APK for $packageName'); + if (isAndroidPlugin(plugin, fileSystem)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + 'apk', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (apk)'); + } + } else { + print('Android is not supported by this plugin'); + } + } + } + } + print('\n\n'); + + if (failingPackages.isNotEmpty) { + print('The following build are failing (see above for details):'); + for (String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + + print('All builds successful!'); + } +} diff --git a/packages/plugin_tools/lib/src/common.dart b/packages/plugin_tools/lib/src/common.dart new file mode 100644 index 000000000000..78b91ee8a75b --- /dev/null +++ b/packages/plugin_tools/lib/src/common.dart @@ -0,0 +1,466 @@ +// 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 'dart:io' as io; +import 'dart:math'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; + +typedef void Print(Object object); + +/// Key for windows platform. +const String kWindows = 'windows'; + +/// Key for macos platform. +const String kMacos = 'macos'; + +/// Key for linux platform. +const String kLinux = 'linux'; + +/// Key for IPA (iOS) platform. +const String kIos = 'ios'; + +/// Key for APK (Android) platform. +const String kAndroid = 'android'; + +/// Key for Web platform. +const String kWeb = 'web'; + +/// Key for IPA. +const String kIpa = 'ipa'; + +/// Key for APK. +const String kApk = 'apk'; + +/// Key for enable experiment. +const String kEnableExperiment = 'enable-experiment'; + +/// Returns whether the given directory contains a Flutter package. +bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) { + if (entity == null || entity is! Directory) { + return false; + } + + try { + final File pubspecFile = + fileSystem.file(p.join(entity.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final YamlMap dependencies = pubspecYaml['dependencies']; + if (dependencies == null) { + return false; + } + return dependencies.containsKey('flutter'); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Returns whether the given directory contains a Flutter [platform] plugin. +/// +/// It checks this by looking for the following pattern in the pubspec: +/// +/// flutter: +/// plugin: +/// platforms: +/// [platform]: +bool pluginSupportsPlatform( + String platform, FileSystemEntity entity, FileSystem fileSystem) { + assert(platform == kIos || + platform == kAndroid || + platform == kWeb || + platform == kMacos || + platform == kWindows || + platform == kLinux); + if (entity == null || entity is! Directory) { + return false; + } + + try { + final File pubspecFile = + fileSystem.file(p.join(entity.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final YamlMap flutterSection = pubspecYaml['flutter']; + if (flutterSection == null) { + return false; + } + final YamlMap pluginSection = flutterSection['plugin']; + if (pluginSection == null) { + return false; + } + final YamlMap platforms = pluginSection['platforms']; + if (platforms == null) { + // Legacy plugin specs are assumed to support iOS and Android. + if (!pluginSection.containsKey('platforms')) { + return platform == kIos || platform == kAndroid; + } + return false; + } + return platforms.containsKey(platform); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Returns whether the given directory contains a Flutter Android plugin. +bool isAndroidPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kAndroid, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter iOS plugin. +bool isIosPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kIos, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter web plugin. +bool isWebPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kWeb, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter Windows plugin. +bool isWindowsPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kWindows, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter macOS plugin. +bool isMacOsPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kMacos, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter linux plugin. +bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kLinux, entity, fileSystem); +} + +/// Error thrown when a command needs to exit with a non-zero exit code. +class ToolExit extends Error { + ToolExit(this.exitCode); + + final int exitCode; +} + +abstract class PluginCommand extends Command { + PluginCommand( + this.packagesDir, + this.fileSystem, { + this.processRunner = const ProcessRunner(), + }) { + argParser.addMultiOption( + _pluginsArg, + splitCommas: true, + help: + 'Specifies which plugins the command should run on (before sharding).', + valueHelp: 'plugin1,plugin2,...', + ); + argParser.addOption( + _shardIndexArg, + help: 'Specifies the zero-based index of the shard to ' + 'which the command applies.', + valueHelp: 'i', + defaultsTo: '0', + ); + argParser.addOption( + _shardCountArg, + help: 'Specifies the number of shards into which plugins are divided.', + valueHelp: 'n', + defaultsTo: '1', + ); + argParser.addMultiOption( + _excludeArg, + abbr: 'e', + help: 'Exclude packages from this command.', + defaultsTo: [], + ); + } + + static const String _pluginsArg = 'plugins'; + static const String _shardIndexArg = 'shardIndex'; + static const String _shardCountArg = 'shardCount'; + static const String _excludeArg = 'exclude'; + + /// The directory containing the plugin packages. + final Directory packagesDir; + + /// The file system. + /// + /// This can be overridden for testing. + final FileSystem fileSystem; + + /// The process runner. + /// + /// This can be overridden for testing. + final ProcessRunner processRunner; + + int _shardIndex; + int _shardCount; + + int get shardIndex { + if (_shardIndex == null) { + checkSharding(); + } + return _shardIndex; + } + + int get shardCount { + if (_shardCount == null) { + checkSharding(); + } + return _shardCount; + } + + void checkSharding() { + final int shardIndex = int.tryParse(argResults[_shardIndexArg]); + final int shardCount = int.tryParse(argResults[_shardCountArg]); + if (shardIndex == null) { + usageException('$_shardIndexArg must be an integer'); + } + if (shardCount == null) { + usageException('$_shardCountArg must be an integer'); + } + if (shardCount < 1) { + usageException('$_shardCountArg must be positive'); + } + if (shardIndex < 0 || shardCount <= shardIndex) { + usageException( + '$_shardIndexArg must be in the half-open range [0..$shardCount['); + } + _shardIndex = shardIndex; + _shardCount = shardCount; + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution. + Stream getPlugins() async* { + // To avoid assuming consistency of `Directory.list` across command + // invocations, we collect and sort the plugin folders before sharding. + // This is considered an implementation detail which is why the API still + // uses streams. + final List allPlugins = await _getAllPlugins().toList(); + allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path)); + // Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2. + // Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3. + // Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0. + final int shardSize = allPlugins.length ~/ shardCount + + (allPlugins.length % shardCount == 0 ? 0 : 1); + final int start = min(shardIndex * shardSize, allPlugins.length); + final int end = min(start + shardSize, allPlugins.length); + + for (Directory plugin in allPlugins.sublist(start, end)) { + yield plugin; + } + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution, assuming there is only one shard. + /// + /// Plugin packages can exist in one of two places relative to the packages + /// directory. + /// + /// 1. As a Dart package in a directory which is a direct child of the + /// packages directory. This is a plugin where all of the implementations + /// exist in a single Dart package. + /// 2. Several plugin packages may live in a directory which is a direct + /// child of the packages directory. This directory groups several Dart + /// packages which implement a single plugin. This directory contains a + /// "client library" package, which declares the API for the plugin, as + /// well as one or more platform-specific implementations. + Stream _getAllPlugins() async* { + final Set plugins = Set.from(argResults[_pluginsArg]); + final Set excludedPlugins = + Set.from(argResults[_excludeArg]); + + await for (FileSystemEntity entity + in packagesDir.list(followLinks: false)) { + // A top-level Dart package is a plugin package. + if (_isDartPackage(entity)) { + if (!excludedPlugins.contains(entity.basename) && + (plugins.isEmpty || plugins.contains(p.basename(entity.path)))) { + yield entity; + } + } else if (entity is Directory) { + // Look for Dart packages under this top-level directory. + await for (FileSystemEntity subdir in entity.list(followLinks: false)) { + if (_isDartPackage(subdir)) { + // If --plugin=my_plugin is passed, then match all federated + // plugins under 'my_plugin'. Also match if the exact plugin is + // passed. + final String relativePath = + p.relative(subdir.path, from: packagesDir.path); + final String basenamePath = p.basename(entity.path); + if (!excludedPlugins.contains(basenamePath) && + !excludedPlugins.contains(relativePath) && + (plugins.isEmpty || + plugins.contains(relativePath) || + plugins.contains(basenamePath))) { + yield subdir; + } + } + } + } + } + } + + /// Returns the example Dart package folders of the plugins involved in this + /// command execution. + Stream getExamples() => + getPlugins().expand(getExamplesForPlugin); + + /// Returns all Dart package folders (typically, plugin + example) of the + /// plugins involved in this command execution. + Stream getPackages() async* { + await for (Directory plugin in getPlugins()) { + yield plugin; + yield* plugin + .list(recursive: true, followLinks: false) + .where(_isDartPackage) + .cast(); + } + } + + /// Returns the files contained, recursively, within the plugins + /// involved in this command execution. + Stream getFiles() { + return getPlugins().asyncExpand((Directory folder) => folder + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => entity is File) + .cast()); + } + + /// Returns whether the specified entity is a directory containing a + /// `pubspec.yaml` file. + bool _isDartPackage(FileSystemEntity entity) { + return entity is Directory && + fileSystem.file(p.join(entity.path, 'pubspec.yaml')).existsSync(); + } + + /// Returns the example Dart packages contained in the specified plugin, or + /// an empty List, if the plugin has no examples. + Iterable getExamplesForPlugin(Directory plugin) { + final Directory exampleFolder = + fileSystem.directory(p.join(plugin.path, 'example')); + if (!exampleFolder.existsSync()) { + return []; + } + if (isFlutterPackage(exampleFolder, fileSystem)) { + return [exampleFolder]; + } + // Only look at the subdirectories of the example directory if the example + // directory itself is not a Dart package, and only look one level below the + // example directory for other dart packages. + return exampleFolder + .listSync() + .where( + (FileSystemEntity entity) => isFlutterPackage(entity, fileSystem)) + .cast(); + } +} + +/// A class used to run processes. +/// +/// We use this instead of directly running the process so it can be overridden +/// in tests. +class ProcessRunner { + const ProcessRunner(); + + /// Run the [executable] with [args] and stream output to stderr and stdout. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// + /// Returns the exit code of the [executable]. + Future runAndStream( + String executable, + List args, { + Directory workingDir, + bool exitOnError = false, + }) async { + print( + 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDir?.path); + await io.stdout.addStream(process.stdout); + await io.stderr.addStream(process.stderr); + if (exitOnError && await process.exitCode != 0) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error See above for details.'); + throw ToolExit(await process.exitCode); + } + return process.exitCode; + } + + /// Run the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// + /// Returns the [io.ProcessResult] of the [executable]. + Future run(String executable, List args, + {Directory workingDir, + bool exitOnError = false, + stdoutEncoding = io.systemEncoding, + stderrEncoding = io.systemEncoding}) async { + return io.Process.run(executable, args, + workingDirectory: workingDir?.path, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding); + } + + /// Starts the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// Returns the started [io.Process]. + Future start(String executable, List args, + {Directory workingDirectory}) async { + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDirectory?.path); + return process; + } + + /// Run the [executable] with [args], throwing an error on non-zero exit code. + /// + /// Unlike [runAndStream], this does not stream the process output to stdout. + /// It also unconditionally throws an error on a non-zero exit code. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// Returns the [io.ProcessResult] of running the [executable]. + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + final io.ProcessResult result = await io.Process.run(executable, args, + workingDirectory: workingDir?.path); + if (result.exitCode != 0) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error Stderr:\n${result.stdout}'); + throw ToolExit(result.exitCode); + } + return result; + } + + String _getErrorString(String executable, List args, + {Directory workingDir}) { + final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; + return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; + } +} diff --git a/packages/plugin_tools/lib/src/create_all_plugins_app_command.dart b/packages/plugin_tools/lib/src/create_all_plugins_app_command.dart new file mode 100644 index 000000000000..0f1431c5aee0 --- /dev/null +++ b/packages/plugin_tools/lib/src/create_all_plugins_app_command.dart @@ -0,0 +1,200 @@ +// 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:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common.dart'; + +// TODO(cyanglaz): Add tests for this command. +// https://github.com/flutter/flutter/issues/61049 +class CreateAllPluginsAppCommand extends PluginCommand { + CreateAllPluginsAppCommand(Directory packagesDir, FileSystem fileSystem) + : super(packagesDir, fileSystem); + + @override + String get description => + 'Generate Flutter app that includes all plugins in packages.'; + + @override + String get name => 'all-plugins-app'; + + @override + Future run() async { + final int exitCode = await _createPlugin(); + if (exitCode != 0) { + throw ToolExit(exitCode); + } + + await Future.wait(>[ + _genPubspecWithAllPlugins(), + _updateAppGradle(), + _updateManifest(), + ]); + } + + Future _createPlugin() async { + final io.ProcessResult result = io.Process.runSync( + 'flutter', + [ + 'create', + '--template=app', + '--project-name=all_plugins', + '--android-language=java', + './all_plugins', + ], + ); + + print(result.stdout); + print(result.stderr); + return result.exitCode; + } + + Future _updateAppGradle() async { + final File gradleFile = fileSystem.file(p.join( + 'all_plugins', + 'android', + 'app', + 'build.gradle', + )); + if (!gradleFile.existsSync()) { + throw ToolExit(64); + } + + final StringBuffer newGradle = StringBuffer(); + for (String line in gradleFile.readAsLinesSync()) { + newGradle.writeln(line); + if (line.contains('defaultConfig {')) { + newGradle.writeln(' multiDexEnabled true'); + } else if (line.contains('dependencies {')) { + newGradle.writeln( + ' implementation \'com.google.guava:guava:27.0.1-android\'\n', + ); + // Tests for https://github.com/flutter/flutter/issues/43383 + newGradle.writeln( + " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", + ); + } + } + gradleFile.writeAsStringSync(newGradle.toString()); + } + + Future _updateManifest() async { + final File manifestFile = fileSystem.file(p.join( + 'all_plugins', + 'android', + 'app', + 'src', + 'main', + 'AndroidManifest.xml', + )); + if (!manifestFile.existsSync()) { + throw ToolExit(64); + } + + final StringBuffer newManifest = StringBuffer(); + for (String line in manifestFile.readAsLinesSync()) { + if (line.contains('package="com.example.all_plugins"')) { + newManifest + ..writeln('package="com.example.all_plugins"') + ..writeln('xmlns:tools="http://schemas.android.com/tools">') + ..writeln() + ..writeln( + '', + ); + } else { + newManifest.writeln(line); + } + } + manifestFile.writeAsStringSync(newManifest.toString()); + } + + Future _genPubspecWithAllPlugins() async { + final Map pluginDeps = + await _getValidPathDependencies(); + final Pubspec pubspec = Pubspec( + 'all_plugins', + description: 'Flutter app containing all 1st party plugins.', + version: Version.parse('1.0.0+1'), + environment: { + 'sdk': VersionConstraint.compatibleWith( + Version.parse('2.0.0'), + ), + }, + dependencies: { + 'flutter': SdkDependency('flutter'), + }..addAll(pluginDeps), + devDependencies: { + 'flutter_test': SdkDependency('flutter'), + }, + dependencyOverrides: pluginDeps, + ); + final File pubspecFile = + fileSystem.file(p.join('all_plugins', 'pubspec.yaml')); + pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); + } + + Future> _getValidPathDependencies() async { + final Map pathDependencies = + {}; + + await for (Directory package in getPlugins()) { + final String pluginName = package.path.split('/').last; + final File pubspecFile = + fileSystem.file(p.join(package.path, 'pubspec.yaml')); + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + + if (pubspec.publishTo != 'none') { + pathDependencies[pluginName] = PathDependency(package.path); + } + } + return pathDependencies; + } + + String _pubspecToString(Pubspec pubspec) { + return ''' +### Generated file. Do not edit. Run `pub global run flutter_plugin_tools gen-pubspec` to update. +name: ${pubspec.name} +description: ${pubspec.description} + +version: ${pubspec.version} + +environment:${_pubspecMapString(pubspec.environment)} + +dependencies:${_pubspecMapString(pubspec.dependencies)} + +dependency_overrides:${_pubspecMapString(pubspec.dependencyOverrides)} + +dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} +###'''; + } + + String _pubspecMapString(Map values) { + final StringBuffer buffer = StringBuffer(); + + for (MapEntry entry in values.entries) { + buffer.writeln(); + if (entry.value is VersionConstraint) { + buffer.write(' ${entry.key}: ${entry.value}'); + } else if (entry.value is SdkDependency) { + final SdkDependency dep = entry.value; + buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); + } else if (entry.value is PathDependency) { + final PathDependency dep = entry.value; + buffer.write(' ${entry.key}: \n path: ${dep.path}'); + } else { + throw UnimplementedError( + 'Not available for type: ${entry.value.runtimeType}', + ); + } + } + + return buffer.toString(); + } +} diff --git a/packages/plugin_tools/lib/src/drive_examples_command.dart b/packages/plugin_tools/lib/src/drive_examples_command.dart new file mode 100644 index 000000000000..8b1fa3624fbf --- /dev/null +++ b/packages/plugin_tools/lib/src/drive_examples_command.dart @@ -0,0 +1,210 @@ +// 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:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'common.dart'; + +class DriveExamplesCommand extends PluginCommand { + DriveExamplesCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag(kLinux, + help: 'Runs the Linux implementation of the examples'); + argParser.addFlag(kMacos, + help: 'Runs the macOS implementation of the examples'); + argParser.addFlag(kWindows, + help: 'Runs the Windows implementation of the examples'); + argParser.addFlag(kIos, + help: 'Runs the iOS implementation of the examples'); + argParser.addFlag(kAndroid, + help: 'Runs the Android implementation of the examples'); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: + 'Runs the driver tests in Dart VM with the given experiments enabled.', + ); + } + + @override + final String name = 'drive-examples'; + + @override + final String description = 'Runs driver tests for plugin example apps.\n\n' + 'For each *_test.dart in test_driver/ it drives an application with a ' + 'corresponding name in the test/ or test_driver/ directories.\n\n' + 'For example, test_driver/app_test.dart would match test/app.dart.\n\n' + 'This command requires "flutter" to be in your path.\n\n' + 'If a file with a corresponding name cannot be found, this driver file' + 'will be used to drive the tests that match ' + 'integration_test/*_test.dart.'; + + @override + Future run() async { + checkSharding(); + final List failingTests = []; + final bool isLinux = argResults[kLinux]; + final bool isMacos = argResults[kMacos]; + final bool isWindows = argResults[kWindows]; + await for (Directory plugin in getPlugins()) { + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + for (Directory example in getExamplesForPlugin(plugin)) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + if (!(await pluginSupportedOnCurrentPlatform(plugin, fileSystem))) { + continue; + } + final Directory driverTests = + fileSystem.directory(p.join(example.path, 'test_driver')); + if (!driverTests.existsSync()) { + // No driver tests available for this example + continue; + } + // Look for driver tests ending in _test.dart in test_driver/ + await for (FileSystemEntity test in driverTests.list()) { + final String driverTestName = + p.relative(test.path, from: driverTests.path); + if (!driverTestName.endsWith('_test.dart')) { + continue; + } + // Try to find a matching app to drive without the _test.dart + final String deviceTestName = driverTestName.replaceAll( + RegExp(r'_test.dart$'), + '.dart', + ); + String deviceTestPath = p.join('test', deviceTestName); + if (!fileSystem + .file(p.join(example.path, deviceTestPath)) + .existsSync()) { + // If the app isn't in test/ folder, look in test_driver/ instead. + deviceTestPath = p.join('test_driver', deviceTestName); + } + + final List targetPaths = []; + if (fileSystem + .file(p.join(example.path, deviceTestPath)) + .existsSync()) { + targetPaths.add(deviceTestPath); + } else { + final Directory integrationTests = + fileSystem.directory(p.join(example.path, 'integration_test')); + + if (await integrationTests.exists()) { + await for (FileSystemEntity integration_test + in integrationTests.list()) { + if (!integration_test.basename.endsWith('_test.dart')) { + continue; + } + targetPaths + .add(p.relative(integration_test.path, from: example.path)); + } + } + + if (targetPaths.isEmpty) { + print(''' +Unable to infer a target application for $driverTestName to drive. +Tried searching for the following: +1. test/$deviceTestName +2. test_driver/$deviceTestName +3. test_driver/*_test.dart +'''); + failingTests.add(p.relative(test.path, from: example.path)); + continue; + } + } + + final List driveArgs = ['drive', '-v']; + + final String enableExperiment = argResults[kEnableExperiment]; + if (enableExperiment.isNotEmpty) { + driveArgs.add('--enable-experiment=$enableExperiment'); + } + + if (isLinux && isLinuxPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'linux', + ]); + } + if (isMacos && isMacOsPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'macos', + ]); + } + if (isWindows && isWindowsPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'windows', + ]); + } + + for (final targetPath in targetPaths) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + ...driveArgs, + '--driver', + p.join('test_driver', driverTestName), + '--target', + targetPath, + ], + workingDir: example, + exitOnError: true); + if (exitCode != 0) { + failingTests.add(p.join(packageName, deviceTestPath)); + } + } + } + } + } + print('\n\n'); + + if (failingTests.isNotEmpty) { + print('The following driver tests are failing (see above for details):'); + for (String test in failingTests) { + print(' * $test'); + } + throw ToolExit(1); + } + + print('All driver tests successful!'); + } + + Future pluginSupportedOnCurrentPlatform( + FileSystemEntity plugin, FileSystem fileSystem) async { + final bool isLinux = argResults[kLinux]; + final bool isMacos = argResults[kMacos]; + final bool isWindows = argResults[kWindows]; + final bool isIOS = argResults[kIos]; + final bool isAndroid = argResults[kAndroid]; + if (isLinux) { + return isLinuxPlugin(plugin, fileSystem); + } + if (isMacos) { + return isMacOsPlugin(plugin, fileSystem); + } + if (isWindows) { + return isWindowsPlugin(plugin, fileSystem); + } + if (isIOS) { + return isIosPlugin(plugin, fileSystem); + } + if (isAndroid) { + return (isAndroidPlugin(plugin, fileSystem)); + } + // When we are here, no flags are specified. Only return true if the plugin supports mobile for legacy command support. + // TODO(cyanglaz): Make mobile platforms flags also required like other platforms (breaking change). + // https://github.com/flutter/flutter/issues/58285 + final bool isMobilePlugin = + isIosPlugin(plugin, fileSystem) || isAndroidPlugin(plugin, fileSystem); + return isMobilePlugin; + } +} diff --git a/packages/plugin_tools/lib/src/firebase_test_lab_command.dart b/packages/plugin_tools/lib/src/firebase_test_lab_command.dart new file mode 100644 index 000000000000..0b4b2a471dbc --- /dev/null +++ b/packages/plugin_tools/lib/src/firebase_test_lab_command.dart @@ -0,0 +1,264 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:uuid/uuid.dart'; + +import 'common.dart'; + +class FirebaseTestLabCommand extends PluginCommand { + FirebaseTestLabCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + Print print = print, + }) : _print = print, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + 'project', + defaultsTo: 'flutter-infra', + help: 'The Firebase project name.', + ); + argParser.addOption('service-key', + defaultsTo: + p.join(io.Platform.environment['HOME'], 'gcloud-service-key.json')); + argParser.addOption('test-run-id', + defaultsTo: Uuid().v4(), + help: + 'Optional string to append to the results path, to avoid conflicts. ' + 'Randomly chosen on each invocation if none is provided. ' + 'The default shown here is just an example.'); + argParser.addMultiOption('device', + splitCommas: false, + defaultsTo: [ + 'model=walleye,version=26', + 'model=flame,version=29' + ], + help: + 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); + argParser.addOption('results-bucket', + defaultsTo: 'gs://flutter_firebase_testlab'); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Enables the given Dart SDK experiments.', + ); + } + + @override + final String name = 'firebase-test-lab'; + + @override + final String description = 'Runs the instrumentation tests of the example ' + 'apps on Firebase Test Lab.\n\n' + 'Runs tests in test_instrumentation folder using the ' + 'instrumentation_test package.'; + + static const String _gradleWrapper = 'gradlew'; + + final Print _print; + + Completer _firebaseProjectConfigured; + + Future _configureFirebaseProject() async { + if (_firebaseProjectConfigured != null) { + return _firebaseProjectConfigured.future; + } else { + _firebaseProjectConfigured = Completer(); + } + await processRunner.runAndExitOnError('gcloud', [ + 'auth', + 'activate-service-account', + '--key-file=${argResults['service-key']}', + ]); + int exitCode = await processRunner.runAndStream('gcloud', [ + 'config', + 'set', + 'project', + argResults['project'], + ]); + if (exitCode == 0) { + _print('\nFirebase project configured.'); + return; + } else { + _print( + '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.'); + } + _firebaseProjectConfigured.complete(null); + } + + @override + Future run() async { + checkSharding(); + final Stream packagesWithTests = getPackages().where( + (Directory d) => + isFlutterPackage(d, fileSystem) && + fileSystem + .directory(p.join( + d.path, 'example', 'android', 'app', 'src', 'androidTest')) + .existsSync()); + + final List failingPackages = []; + final List missingFlutterBuild = []; + int resultsCounter = + 0; // We use a unique GCS bucket for each Firebase Test Lab run + await for (Directory package in packagesWithTests) { + // See https://github.com/flutter/flutter/issues/38983 + + final Directory exampleDirectory = + fileSystem.directory(p.join(package.path, 'example')); + final String packageName = + p.relative(package.path, from: packagesDir.path); + _print('\nRUNNING FIREBASE TEST LAB TESTS for $packageName'); + + final Directory androidDirectory = + fileSystem.directory(p.join(exampleDirectory.path, 'android')); + + final String enableExperiment = argResults[kEnableExperiment]; + final String encodedEnableExperiment = + Uri.encodeComponent('--enable-experiment=$enableExperiment'); + + // Ensures that gradle wrapper exists + if (!fileSystem + .file(p.join(androidDirectory.path, _gradleWrapper)) + .existsSync()) { + final int exitCode = await processRunner.runAndStream( + 'flutter', + [ + 'build', + 'apk', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + continue; + } + + await _configureFirebaseProject(); + + int exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + [ + 'app:assembleAndroidTest', + '-Pverbose=true', + if (enableExperiment.isNotEmpty) + '-Pextra-front-end-options=$encodedEnableExperiment', + if (enableExperiment.isNotEmpty) + '-Pextra-gen-snapshot-options=$encodedEnableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + + // Look for tests recursively in folders that start with 'test' and that + // live in the root or example folders. + bool isTestDir(FileSystemEntity dir) { + return p.basename(dir.path).startsWith('test') || + p.basename(dir.path) == 'integration_test'; + } + + final List testDirs = + package.listSync().where(isTestDir).toList(); + final Directory example = + fileSystem.directory(p.join(package.path, 'example')); + testDirs.addAll(example.listSync().where(isTestDir).toList()); + for (Directory testDir in testDirs) { + bool isE2ETest(FileSystemEntity file) { + return file.path.endsWith('_e2e.dart') || + (file.parent.basename == 'integration_test' && + file.path.endsWith('_test.dart')); + } + + final List testFiles = testDir + .listSync(recursive: true, followLinks: true) + .where(isE2ETest) + .toList(); + for (FileSystemEntity test in testFiles) { + exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + [ + 'app:assembleDebug', + '-Pverbose=true', + '-Ptarget=${test.path}', + if (enableExperiment.isNotEmpty) + '-Pextra-front-end-options=$encodedEnableExperiment', + if (enableExperiment.isNotEmpty) + '-Pextra-gen-snapshot-options=$encodedEnableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + final String buildId = io.Platform.environment['CIRRUS_BUILD_ID']; + final String testRunId = argResults['test-run-id']; + final String resultsDir = + 'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/'; + final List args = [ + 'firebase', + 'test', + 'android', + 'run', + '--type', + 'instrumentation', + '--app', + 'build/app/outputs/apk/debug/app-debug.apk', + '--test', + 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', + '--timeout', + '5m', + '--results-bucket=${argResults['results-bucket']}', + '--results-dir=${resultsDir}', + ]; + for (String device in argResults['device']) { + args.addAll(['--device', device]); + } + exitCode = await processRunner.runAndStream('gcloud', args, + workingDir: exampleDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + } + } + } + + _print('\n\n'); + if (failingPackages.isNotEmpty) { + _print( + 'The instrumentation tests for the following packages are failing (see above for' + 'details):'); + for (String package in failingPackages) { + _print(' * $package'); + } + } + if (missingFlutterBuild.isNotEmpty) { + _print('Run "pub global run flutter_plugin_tools build-examples --apk" on' + 'the following packages before executing tests again:'); + for (String package in missingFlutterBuild) { + _print(' * $package'); + } + } + + if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { + throw ToolExit(1); + } + + _print('All Firebase Test Lab tests successful!'); + } +} diff --git a/packages/plugin_tools/lib/src/format_command.dart b/packages/plugin_tools/lib/src/format_command.dart new file mode 100644 index 000000000000..ec326b96c1f9 --- /dev/null +++ b/packages/plugin_tools/lib/src/format_command.dart @@ -0,0 +1,147 @@ +// 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 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as p; +import 'package:quiver/iterables.dart'; + +import 'common.dart'; + +const String _googleFormatterUrl = + 'https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'; + +class FormatCommand extends PluginCommand { + FormatCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag('travis', hide: true); + argParser.addOption('clang-format', + defaultsTo: 'clang-format', + help: 'Path to executable of clang-format v5.'); + } + + @override + final String name = 'format'; + + @override + final String description = + 'Formats the code of all packages (Java, Objective-C, C++, and Dart).\n\n' + 'This command requires "git", "flutter" and "clang-format" v5 to be in ' + 'your path.'; + + @override + Future run() async { + checkSharding(); + final String googleFormatterPath = await _getGoogleFormatterPath(); + + await _formatDart(); + await _formatJava(googleFormatterPath); + await _formatCppAndObjectiveC(); + + if (argResults['travis']) { + final bool modified = await _didModifyAnything(); + if (modified) { + throw ToolExit(1); + } + } + } + + Future _didModifyAnything() async { + final io.ProcessResult modifiedFiles = await processRunner + .runAndExitOnError('git', ['ls-files', '--modified'], + workingDir: packagesDir); + + print('\n\n'); + + if (modifiedFiles.stdout.isEmpty) { + print('All files formatted correctly.'); + return false; + } + + print('These files are not formatted correctly (see diff below):'); + LineSplitter.split(modifiedFiles.stdout) + .map((String line) => ' $line') + .forEach(print); + + print('\nTo fix run "pub global activate flutter_plugin_tools && ' + 'pub global run flutter_plugin_tools format" or copy-paste ' + 'this command into your terminal:'); + + print('patch -p1 <['diff'], workingDir: packagesDir); + print(diff.stdout); + print('DONE'); + return true; + } + + Future _formatCppAndObjectiveC() async { + print('Formatting all .cc, .cpp, .mm, .m, and .h files...'); + final Iterable allFiles = [] + ..addAll(await _getFilesWithExtension('.h')) + ..addAll(await _getFilesWithExtension('.m')) + ..addAll(await _getFilesWithExtension('.mm')) + ..addAll(await _getFilesWithExtension('.cc')) + ..addAll(await _getFilesWithExtension('.cpp')); + // Split this into multiple invocations to avoid a + // 'ProcessException: Argument list too long'. + final Iterable> batches = partition(allFiles, 100); + for (List batch in batches) { + await processRunner.runAndStream(argResults['clang-format'], + ['-i', '--style=Google']..addAll(batch), + workingDir: packagesDir, exitOnError: true); + } + } + + Future _formatJava(String googleFormatterPath) async { + print('Formatting all .java files...'); + final Iterable javaFiles = await _getFilesWithExtension('.java'); + await processRunner.runAndStream('java', + ['-jar', googleFormatterPath, '--replace']..addAll(javaFiles), + workingDir: packagesDir, exitOnError: true); + } + + Future _formatDart() async { + // This actually should be fine for non-Flutter Dart projects, no need to + // specifically shell out to dartfmt -w in that case. + print('Formatting all .dart files...'); + final Iterable dartFiles = await _getFilesWithExtension('.dart'); + if (dartFiles.isEmpty) { + print( + 'No .dart files to format. If you set the `--exclude` flag, most likey they were skipped'); + } else { + await processRunner.runAndStream( + 'flutter', ['format']..addAll(dartFiles), + workingDir: packagesDir, exitOnError: true); + } + } + + Future> _getFilesWithExtension(String extension) async => + getFiles() + .where((File file) => p.extension(file.path) == extension) + .map((File file) => file.path) + .toList(); + + Future _getGoogleFormatterPath() async { + final String javaFormatterPath = p.join( + p.dirname(p.fromUri(io.Platform.script)), + 'google-java-format-1.3-all-deps.jar'); + final File javaFormatterFile = fileSystem.file(javaFormatterPath); + + if (!javaFormatterFile.existsSync()) { + print('Downloading Google Java Format...'); + final http.Response response = await http.get(_googleFormatterUrl); + javaFormatterFile.writeAsBytesSync(response.bodyBytes); + } + + return javaFormatterPath; + } +} diff --git a/packages/plugin_tools/lib/src/java_test_command.dart b/packages/plugin_tools/lib/src/java_test_command.dart new file mode 100644 index 000000000000..cf605bfc5ce2 --- /dev/null +++ b/packages/plugin_tools/lib/src/java_test_command.dart @@ -0,0 +1,89 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class JavaTestCommand extends PluginCommand { + JavaTestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner); + + @override + final String name = 'java-test'; + + @override + final String description = 'Runs the Java tests of the example apps.\n\n' + 'Building the apks of the example apps is required before executing this' + 'command.'; + + static const String _gradleWrapper = 'gradlew'; + + @override + Future run() async { + checkSharding(); + final Stream examplesWithTests = getExamples().where( + (Directory d) => + isFlutterPackage(d, fileSystem) && + fileSystem + .directory(p.join(d.path, 'android', 'app', 'src', 'test')) + .existsSync()); + + final List failingPackages = []; + final List missingFlutterBuild = []; + await for (Directory example in examplesWithTests) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + print('\nRUNNING JAVA TESTS for $packageName'); + + final Directory androidDirectory = + fileSystem.directory(p.join(example.path, 'android')); + if (!fileSystem + .file(p.join(androidDirectory.path, _gradleWrapper)) + .existsSync()) { + print('ERROR: Run "flutter build apk" on example app of $packageName' + 'before executing tests.'); + missingFlutterBuild.add(packageName); + continue; + } + + final int exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + ['testDebugUnitTest', '--info'], + workingDir: androidDirectory); + if (exitCode != 0) { + failingPackages.add(packageName); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print( + 'The Java tests for the following packages are failing (see above for' + 'details):'); + for (String package in failingPackages) { + print(' * $package'); + } + } + if (missingFlutterBuild.isNotEmpty) { + print('Run "pub global run flutter_plugin_tools build-examples --apk" on' + 'the following packages before executing tests again:'); + for (String package in missingFlutterBuild) { + print(' * $package'); + } + } + + if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { + throw ToolExit(1); + } + + print('All Java tests successful!'); + } +} diff --git a/packages/plugin_tools/lib/src/lint_podspecs_command.dart b/packages/plugin_tools/lib/src/lint_podspecs_command.dart new file mode 100644 index 000000000000..68fd4b61dd66 --- /dev/null +++ b/packages/plugin_tools/lib/src/lint_podspecs_command.dart @@ -0,0 +1,146 @@ +// 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 'dart:convert'; +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'common.dart'; + +typedef void Print(Object object); + +/// Lint the CocoaPod podspecs, run the static analyzer on iOS/macOS plugin +/// platform code, and run unit tests. +/// +/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +class LintPodspecsCommand extends PluginCommand { + LintPodspecsCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), + Print print = print, + }) : _print = print, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addMultiOption('skip', + help: + 'Skip all linting for podspecs with this basename (example: federated plugins with placeholder podspecs)', + valueHelp: 'podspec_file_name'); + argParser.addMultiOption('ignore-warnings', + help: + 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs with this basename (example: plugins with known warnings)', + valueHelp: 'podspec_file_name'); + argParser.addMultiOption('no-analyze', + help: + 'Do not pass --analyze flag to "pod lib lint" for podspecs with this basename (example: plugins with known analyzer warnings)', + valueHelp: 'podspec_file_name'); + } + + @override + final String name = 'podspecs'; + + @override + List get aliases => ['podspec']; + + @override + final String description = + 'Runs "pod lib lint" on all iOS and macOS plugin podspecs.\n\n' + 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; + + final Platform platform; + + final Print _print; + + @override + Future run() async { + if (!platform.isMacOS) { + _print('Detected platform is not macOS, skipping podspec lint'); + return; + } + + checkSharding(); + + await processRunner.runAndExitOnError('which', ['pod'], + workingDir: packagesDir); + + _print('Starting podspec lint test'); + + final List failingPlugins = []; + for (File podspec in await _podspecsToLint()) { + if (!await _lintPodspec(podspec)) { + failingPlugins.add(p.basenameWithoutExtension(podspec.path)); + } + } + + _print('\n\n'); + if (failingPlugins.isNotEmpty) { + _print('The following plugins have podspec errors (see above):'); + failingPlugins.forEach((String plugin) { + _print(' * $plugin'); + }); + throw ToolExit(1); + } + } + + Future> _podspecsToLint() async { + final List podspecs = await getFiles().where((File entity) { + final String filePath = entity.path; + return p.extension(filePath) == '.podspec' && + !argResults['skip'].contains(p.basenameWithoutExtension(filePath)); + }).toList(); + + podspecs.sort( + (File a, File b) => p.basename(a.path).compareTo(p.basename(b.path))); + return podspecs; + } + + Future _lintPodspec(File podspec) async { + // Do not run the static analyzer on plugins with known analyzer issues. + final String podspecPath = podspec.path; + final bool runAnalyzer = !argResults['no-analyze'] + .contains(p.basenameWithoutExtension(podspecPath)); + + final String podspecBasename = p.basename(podspecPath); + if (runAnalyzer) { + _print('Linting and analyzing $podspecBasename'); + } else { + _print('Linting $podspecBasename'); + } + + // Lint plugin as framework (use_frameworks!). + final ProcessResult frameworkResult = await _runPodLint(podspecPath, + runAnalyzer: runAnalyzer, libraryLint: true); + _print(frameworkResult.stdout); + _print(frameworkResult.stderr); + + // Lint plugin as library. + final ProcessResult libraryResult = await _runPodLint(podspecPath, + runAnalyzer: runAnalyzer, libraryLint: false); + _print(libraryResult.stdout); + _print(libraryResult.stderr); + + return frameworkResult.exitCode == 0 && libraryResult.exitCode == 0; + } + + Future _runPodLint(String podspecPath, + {bool runAnalyzer, bool libraryLint}) async { + final bool allowWarnings = argResults['ignore-warnings'] + .contains(p.basenameWithoutExtension(podspecPath)); + final List arguments = [ + 'lib', + 'lint', + podspecPath, + if (allowWarnings) '--allow-warnings', + if (runAnalyzer) '--analyze', + if (libraryLint) '--use-libraries' + ]; + + return processRunner.run('pod', arguments, + workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); + } +} diff --git a/packages/plugin_tools/lib/src/list_command.dart b/packages/plugin_tools/lib/src/list_command.dart new file mode 100644 index 000000000000..7f94daac7096 --- /dev/null +++ b/packages/plugin_tools/lib/src/list_command.dart @@ -0,0 +1,60 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:file/file.dart'; + +import 'common.dart'; + +class ListCommand extends PluginCommand { + ListCommand(Directory packagesDir, FileSystem fileSystem) + : super(packagesDir, fileSystem) { + argParser.addOption( + _type, + defaultsTo: _plugin, + allowed: [_plugin, _example, _package, _file], + help: 'What type of file system content to list.', + ); + } + + static const String _type = 'type'; + static const String _plugin = 'plugin'; + static const String _example = 'example'; + static const String _package = 'package'; + static const String _file = 'file'; + + @override + final String name = 'list'; + + @override + final String description = 'Lists packages or files'; + + @override + Future run() async { + checkSharding(); + switch (argResults[_type]) { + case _plugin: + await for (Directory package in getPlugins()) { + print(package.path); + } + break; + case _example: + await for (Directory package in getExamples()) { + print(package.path); + } + break; + case _package: + await for (Directory package in getPackages()) { + print(package.path); + } + break; + case _file: + await for (File file in getFiles()) { + print(file.path); + } + break; + } + } +} diff --git a/packages/plugin_tools/lib/src/main.dart b/packages/plugin_tools/lib/src/main.dart new file mode 100644 index 000000000000..bb3f67c0a9e1 --- /dev/null +++ b/packages/plugin_tools/lib/src/main.dart @@ -0,0 +1,63 @@ +// 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:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; +import 'package:path/path.dart' as p; + +import 'analyze_command.dart'; +import 'build_examples_command.dart'; +import 'common.dart'; +import 'create_all_plugins_app_command.dart'; +import 'drive_examples_command.dart'; +import 'firebase_test_lab_command.dart'; +import 'format_command.dart'; +import 'java_test_command.dart'; +import 'lint_podspecs_command.dart'; +import 'list_command.dart'; +import 'test_command.dart'; +import 'version_check_command.dart'; +import 'xctest_command.dart'; + +void main(List args) { + final FileSystem fileSystem = const LocalFileSystem(); + + Directory packagesDir = fileSystem + .directory(p.join(fileSystem.currentDirectory.path, 'packages')); + + if (!packagesDir.existsSync()) { + if (p.basename(fileSystem.currentDirectory.path) == 'packages') { + packagesDir = fileSystem.currentDirectory; + } else { + print('Error: Cannot find a "packages" sub-directory'); + io.exit(1); + } + } + + final CommandRunner commandRunner = CommandRunner( + 'pub global run flutter_plugin_tools', + 'Productivity utils for hosting multiple plugins within one repository.') + ..addCommand(AnalyzeCommand(packagesDir, fileSystem)) + ..addCommand(BuildExamplesCommand(packagesDir, fileSystem)) + ..addCommand(CreateAllPluginsAppCommand(packagesDir, fileSystem)) + ..addCommand(DriveExamplesCommand(packagesDir, fileSystem)) + ..addCommand(FirebaseTestLabCommand(packagesDir, fileSystem)) + ..addCommand(FormatCommand(packagesDir, fileSystem)) + ..addCommand(JavaTestCommand(packagesDir, fileSystem)) + ..addCommand(LintPodspecsCommand(packagesDir, fileSystem)) + ..addCommand(ListCommand(packagesDir, fileSystem)) + ..addCommand(PublishPluginCommand(packagesDir, fileSystem)) + ..addCommand(TestCommand(packagesDir, fileSystem)) + ..addCommand(VersionCheckCommand(packagesDir, fileSystem)) + ..addCommand(XCTestCommand(packagesDir, fileSystem)); + + commandRunner.run(args).catchError((Object e) { + final ToolExit toolExit = e; + io.exit(toolExit.exitCode); + }, test: (Object e) => e is ToolExit); +} diff --git a/packages/plugin_tools/lib/src/publish_plugin_command.dart b/packages/plugin_tools/lib/src/publish_plugin_command.dart new file mode 100644 index 000000000000..55c48f5484e5 --- /dev/null +++ b/packages/plugin_tools/lib/src/publish_plugin_command.dart @@ -0,0 +1,227 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; + +import 'common.dart'; + +/// Wraps pub publish with a few niceties used by the flutter/plugin team. +/// +/// 1. Checks for any modified files in git and refuses to publish if there's an +/// issue. +/// 2. Tags the release with the format -v. +/// 3. Pushes the release to a remote. +/// +/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full +/// usage information. +/// +/// [processRunner], [print], and [stdin] can be overriden for easier testing. +class PublishPluginCommand extends PluginCommand { + PublishPluginCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + Print print = print, + Stdin stdinput, + }) : _print = print, + _stdin = stdinput ?? stdin, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + _packageOption, + help: 'The package to publish.' + 'If the package directory name is different than its pubspec.yaml name, then this should specify the directory.', + ); + argParser.addMultiOption(_pubFlagsOption, + help: + 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); + argParser.addFlag( + _tagReleaseOption, + help: 'Whether or not to tag the release.', + defaultsTo: true, + negatable: true, + ); + argParser.addFlag( + _pushTagsOption, + help: + 'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.', + defaultsTo: true, + negatable: true, + ); + argParser.addOption( + _remoteOption, + help: + 'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.', + // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. + defaultsTo: 'upstream', + ); + } + + static const String _packageOption = 'package'; + static const String _tagReleaseOption = 'tag-release'; + static const String _pushTagsOption = 'push-tags'; + static const String _pubFlagsOption = 'pub-publish-flags'; + static const String _remoteOption = 'remote'; + + // Version tags should follow -v. For example, + // `flutter_plugin_tools-v0.0.24`. + static const String _tagFormat = '%PACKAGE%-v%VERSION%'; + + @override + final String name = 'publish-plugin'; + + @override + final String description = + 'Attempts to publish the given plugin and tag its release on GitHub.'; + + final Print _print; + final Stdin _stdin; + // The directory of the actual package that we are publishing. + Directory _packageDir; + StreamSubscription _stdinSubscription; + + @override + Future run() async { + checkSharding(); + _print('Checking local repo...'); + _packageDir = _checkPackageDir(); + await _checkGitStatus(); + final bool shouldPushTag = argResults[_pushTagsOption]; + final String remote = argResults[_remoteOption]; + String remoteUrl; + if (shouldPushTag) { + remoteUrl = await _verifyRemote(remote); + } + _print('Local repo is ready!'); + + await _publish(); + _print('Package published!'); + if (!argResults[_tagReleaseOption]) { + return await _finishSuccesfully(); + } + + _print('Tagging release...'); + final String tag = _getTag(); + await processRunner.runAndExitOnError('git', ['tag', tag], + workingDir: _packageDir); + if (!shouldPushTag) { + return await _finishSuccesfully(); + } + + _print('Pushing tag to $remote...'); + await _pushTagToRemote(remote: remote, tag: tag, remoteUrl: remoteUrl); + await _finishSuccesfully(); + } + + Future _finishSuccesfully() async { + await _stdinSubscription.cancel(); + _print('Done!'); + } + + Directory _checkPackageDir() { + final String package = argResults[_packageOption]; + if (package == null) { + _print( + 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); + throw ToolExit(1); + } + final Directory _packageDir = packagesDir.childDirectory(package); + if (!_packageDir.existsSync()) { + _print('${_packageDir.absolute.path} does not exist.'); + throw ToolExit(1); + } + if (!isFlutterPackage(_packageDir, fileSystem)) { + _print('${_packageDir.absolute.path} is not a flutter package.'); + throw ToolExit(1); + } + return _packageDir; + } + + Future _checkGitStatus() async { + if (!await GitDir.isGitDir(packagesDir.path)) { + _print('$packagesDir is not a valid Git repository.'); + throw ToolExit(1); + } + + final ProcessResult statusResult = await processRunner.runAndExitOnError( + 'git', + [ + 'status', + '--porcelain', + '--ignored', + _packageDir.absolute.path + ], + workingDir: _packageDir); + final String statusOutput = statusResult.stdout; + if (statusOutput.isNotEmpty) { + _print( + "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" + '$statusOutput\n' + 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); + throw ToolExit(1); + } + } + + Future _verifyRemote(String remote) async { + final ProcessResult remoteInfo = await processRunner.runAndExitOnError( + 'git', ['remote', 'get-url', remote], + workingDir: _packageDir); + return remoteInfo.stdout; + } + + Future _publish() async { + final List publishFlags = argResults[_pubFlagsOption]; + _print( + 'Running `pub publish ${publishFlags.join(' ')}` in ${_packageDir.absolute.path}...\n'); + final Process publish = await processRunner.start( + 'flutter', ['pub', 'publish'] + publishFlags, + workingDirectory: _packageDir); + publish.stdout + .transform(utf8.decoder) + .listen((String data) => _print(data)); + publish.stderr + .transform(utf8.decoder) + .listen((String data) => _print(data)); + _stdinSubscription = _stdin + .transform(utf8.decoder) + .listen((String data) => publish.stdin.writeln(data)); + final int result = await publish.exitCode; + if (result != 0) { + _print('Publish failed. Exiting.'); + throw ToolExit(result); + } + } + + String _getTag() { + final File pubspecFile = + fileSystem.file(p.join(_packageDir.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final String name = pubspecYaml['name']; + final String version = pubspecYaml['version']; + // We should have failed to publish if these were unset. + assert(name.isNotEmpty && version.isNotEmpty); + return _tagFormat + .replaceAll('%PACKAGE%', name) + .replaceAll('%VERSION%', version); + } + + Future _pushTagToRemote( + {@required String remote, + @required String tag, + @required String remoteUrl}) async { + assert(remote != null && tag != null && remoteUrl != null); + _print('Ready to push $tag to $remoteUrl (y/n)?'); + final String input = _stdin.readLineSync(); + if (input.toLowerCase() != 'y') { + _print('Tag push canceled.'); + throw ToolExit(1); + } + + await processRunner.runAndExitOnError('git', ['push', remote, tag], + workingDir: packagesDir); + } +} diff --git a/packages/plugin_tools/lib/src/test_command.dart b/packages/plugin_tools/lib/src/test_command.dart new file mode 100644 index 000000000000..e938168cfa89 --- /dev/null +++ b/packages/plugin_tools/lib/src/test_command.dart @@ -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. + +import 'dart:async'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class TestCommand extends PluginCommand { + TestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Runs the tests in Dart VM with the given experiments enabled.', + ); + } + + @override + final String name = 'test'; + + @override + final String description = 'Runs the Dart tests for all packages.\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + final List failingPackages = []; + await for (Directory packageDir in getPackages()) { + final String packageName = + p.relative(packageDir.path, from: packagesDir.path); + if (!fileSystem.directory(p.join(packageDir.path, 'test')).existsSync()) { + print('SKIPPING $packageName - no test subdirectory'); + continue; + } + + print('RUNNING $packageName tests...'); + + final String enableExperiment = argResults[kEnableExperiment]; + + // `flutter test` automatically gets packages. `pub run test` does not. :( + int exitCode = 0; + if (isFlutterPackage(packageDir, fileSystem)) { + final List args = [ + 'test', + '--color', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ]; + + if (isWebPlugin(packageDir, fileSystem)) { + args.add('--platform=chrome'); + } + exitCode = await processRunner.runAndStream( + 'flutter', + args, + workingDir: packageDir, + ); + } else { + exitCode = await processRunner.runAndStream( + 'pub', + ['get'], + workingDir: packageDir, + ); + if (exitCode == 0) { + exitCode = await processRunner.runAndStream( + 'pub', + [ + 'run', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + 'test', + ], + workingDir: packageDir, + ); + } + } + if (exitCode != 0) { + failingPackages.add(packageName); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print('Tests for the following packages are failing (see above):'); + failingPackages.forEach((String package) { + print(' * $package'); + }); + throw ToolExit(1); + } + + print('All tests are passing!'); + } +} diff --git a/packages/plugin_tools/lib/src/version_check_command.dart b/packages/plugin_tools/lib/src/version_check_command.dart new file mode 100644 index 000000000000..2c6b92bbcb7a --- /dev/null +++ b/packages/plugin_tools/lib/src/version_check_command.dart @@ -0,0 +1,220 @@ +// 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 'dart:io' as io; + +import 'package:meta/meta.dart'; +import 'package:colorize/colorize.dart'; +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:yaml/yaml.dart'; + +import 'common.dart'; + +const String _kBaseSha = 'base_sha'; + +class GitVersionFinder { + GitVersionFinder(this.baseGitDir, this.baseSha); + + final GitDir baseGitDir; + final String baseSha; + + static bool isPubspec(String file) { + return file.trim().endsWith('pubspec.yaml'); + } + + Future> getChangedPubSpecs() async { + final io.ProcessResult changedFilesCommand = await baseGitDir + .runCommand(['diff', '--name-only', '$baseSha', 'HEAD']); + final List changedFiles = + changedFilesCommand.stdout.toString().split('\n'); + return changedFiles.where(isPubspec).toList(); + } + + Future getPackageVersion(String pubspecPath, String gitRef) async { + final io.ProcessResult gitShow = + await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); + final String fileContent = gitShow.stdout; + final String versionString = loadYaml(fileContent)['version']; + return versionString == null ? null : Version.parse(versionString); + } +} + +enum NextVersionType { + BREAKING_MAJOR, + MAJOR_NULLSAFETY_PRE_RELEASE, + MINOR_NULLSAFETY_PRE_RELEASE, + MINOR, + PATCH, + RELEASE, +} + +Version getNextNullSafetyPreRelease(Version current, Version next) { + String nextNullsafetyPrerelease = 'nullsafety'; + if (current.isPreRelease && + current.preRelease.first is String && + current.preRelease.first == 'nullsafety') { + if (current.preRelease.length == 1) { + nextNullsafetyPrerelease = 'nullsafety.1'; + } else if (current.preRelease.length == 2 && + current.preRelease.last is int) { + nextNullsafetyPrerelease = 'nullsafety.${current.preRelease.last + 1}'; + } + } + return Version( + next.major, + next.minor, + next.patch, + pre: nextNullsafetyPrerelease, + ); +} + +@visibleForTesting +Map getAllowedNextVersions( + Version masterVersion, Version headVersion) { + final Version nextNullSafetyMajor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMajor); + final Version nextNullSafetyMinor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); + final Map allowedNextVersions = + { + masterVersion.nextMajor: NextVersionType.BREAKING_MAJOR, + nextNullSafetyMajor: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE, + nextNullSafetyMinor: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE, + masterVersion.nextMinor: NextVersionType.MINOR, + masterVersion.nextPatch: NextVersionType.PATCH, + }; + + if (masterVersion.major < 1 && headVersion.major < 1) { + int nextBuildNumber = -1; + if (masterVersion.build.isEmpty) { + nextBuildNumber = 1; + } else { + final int currentBuildNumber = masterVersion.build.first; + nextBuildNumber = currentBuildNumber + 1; + } + final Version preReleaseVersion = Version( + masterVersion.major, + masterVersion.minor, + masterVersion.patch, + build: nextBuildNumber.toString(), + ); + allowedNextVersions.clear(); + allowedNextVersions[masterVersion.nextMajor] = NextVersionType.RELEASE; + allowedNextVersions[masterVersion.nextMinor] = + NextVersionType.BREAKING_MAJOR; + allowedNextVersions[masterVersion.nextPatch] = NextVersionType.MINOR; + allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; + + final Version nextNullSafetyMajor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); + final Version nextNullSafetyMinor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextPatch); + + allowedNextVersions[nextNullSafetyMajor] = + NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE; + allowedNextVersions[nextNullSafetyMinor] = + NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE; + } + return allowedNextVersions; +} + +class VersionCheckCommand extends PluginCommand { + VersionCheckCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + this.gitDir, + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption(_kBaseSha); + } + + /// The git directory to use. By default it uses the parent directory. + /// + /// This can be mocked for testing. + final GitDir gitDir; + + @override + final String name = 'version-check'; + + @override + final String description = + 'Checks if the versions of the plugins have been incremented per pub specification.\n\n' + 'This command requires "pub" and "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + + final String rootDir = packagesDir.parent.absolute.path; + final String baseSha = argResults[_kBaseSha]; + + GitDir baseGitDir = gitDir; + if (baseGitDir == null) { + if (!await GitDir.isGitDir(rootDir)) { + print('$rootDir is not a valid Git repository.'); + throw ToolExit(2); + } + baseGitDir = await GitDir.fromExisting(rootDir); + } + + final GitVersionFinder gitVersionFinder = + GitVersionFinder(baseGitDir, baseSha); + + final List changedPubspecs = + await gitVersionFinder.getChangedPubSpecs(); + + for (final String pubspecPath in changedPubspecs) { + try { + final File pubspecFile = fileSystem.file(pubspecPath); + if (!pubspecFile.existsSync()) { + continue; + } + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec.publishTo == 'none') { + continue; + } + + final Version masterVersion = + await gitVersionFinder.getPackageVersion(pubspecPath, baseSha); + final Version headVersion = + await gitVersionFinder.getPackageVersion(pubspecPath, 'HEAD'); + if (headVersion == null) { + continue; // Example apps don't have versions + } + + final Map allowedNextVersions = + getAllowedNextVersions(masterVersion, headVersion); + + if (!allowedNextVersions.containsKey(headVersion)) { + final String error = '$pubspecPath incorrectly updated version.\n' + 'HEAD: $headVersion, master: $masterVersion.\n' + 'Allowed versions: $allowedNextVersions'; + final Colorize redError = Colorize(error)..red(); + print(redError); + throw ToolExit(1); + } + + bool isPlatformInterface = pubspec.name.endsWith("_platform_interface"); + if (isPlatformInterface && + allowedNextVersions[headVersion] == + NextVersionType.BREAKING_MAJOR) { + final String error = '$pubspecPath breaking change detected.\n' + 'Breaking changes to platform interfaces are strongly discouraged.\n'; + final Colorize redError = Colorize(error)..red(); + print(redError); + throw ToolExit(1); + } + } on io.ProcessException { + print('Unable to find pubspec in master for $pubspecPath.' + ' Safe to ignore if the project is new.'); + } + } + + print('No version check errors found!'); + } +} diff --git a/packages/plugin_tools/lib/src/xctest_command.dart b/packages/plugin_tools/lib/src/xctest_command.dart new file mode 100644 index 000000000000..d90b7a8fbfea --- /dev/null +++ b/packages/plugin_tools/lib/src/xctest_command.dart @@ -0,0 +1,216 @@ +// 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 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +const String _kiOSDestination = 'ios-destination'; +const String _kTarget = 'target'; +const String _kSkip = 'skip'; +const String _kXcodeBuildCommand = 'xcodebuild'; +const String _kXCRunCommand = 'xcrun'; +const String _kFoundNoSimulatorsMessage = + 'Cannot find any available simulators, tests failed'; + +/// The command to run iOS' XCTests in plugins, this should work for both XCUnitTest and XCUITest targets. +/// The tests target have to be added to the xcode project of the example app. Usually at "example/ios/Runner.xcodeproj". +/// The command takes a "-target" argument which has to match the target of the test target. +/// For information on how to add test target in an xcode project, see https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/UnitTesting.html +class XCTestCommand extends PluginCommand { + XCTestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + _kiOSDestination, + help: + 'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n' + 'this is passed to the `-destination` argument in xcodebuild command.\n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', + ); + argParser.addOption(_kTarget, + help: 'The test target.\n' + 'This is the xcode project test target. This is passed to the `-scheme` argument in the xcodebuild command. \n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the scheme'); + argParser.addMultiOption(_kSkip, + help: 'Plugins to skip while running this command. \n'); + } + + @override + final String name = 'xctest'; + + @override + final String description = 'Runs the xctests in the iOS example apps.\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + if (argResults[_kTarget] == null) { + // TODO(cyanglaz): Automatically find all the available testing schemes if this argument is not specified. + // https://github.com/flutter/flutter/issues/68419 + print('--$_kTarget must be specified'); + throw ToolExit(1); + } + + String destination = argResults[_kiOSDestination]; + if (destination == null) { + String simulatorId = await _findAvailableIphoneSimulator(); + if (simulatorId == null) { + print(_kFoundNoSimulatorsMessage); + throw ToolExit(1); + } + destination = 'id=$simulatorId'; + } + + checkSharding(); + + final String target = argResults[_kTarget]; + final List skipped = argResults[_kSkip]; + + List failingPackages = []; + await for (Directory plugin in getPlugins()) { + // Start running for package. + final String packageName = + p.relative(plugin.path, from: packagesDir.path); + print('Start running for $packageName ...'); + if (!isIosPlugin(plugin, fileSystem)) { + print('iOS is not supported by this plugin.'); + print('\n\n'); + continue; + } + if (skipped.contains(packageName)) { + print('$packageName was skipped with the --skip flag.'); + print('\n\n'); + continue; + } + for (Directory example in getExamplesForPlugin(plugin)) { + // Look for the test scheme in the example app. + print('Look for target named: $_kTarget ...'); + final List findSchemeArgs = [ + '-project', + 'ios/Runner.xcodeproj', + '-list', + '-json' + ]; + final String completeFindSchemeCommand = + '$_kXcodeBuildCommand ${findSchemeArgs.join(' ')}'; + print(completeFindSchemeCommand); + final io.ProcessResult xcodeprojListResult = await processRunner + .run(_kXcodeBuildCommand, findSchemeArgs, workingDir: example); + if (xcodeprojListResult.exitCode != 0) { + print('Error occurred while running "$completeFindSchemeCommand":\n' + '${xcodeprojListResult.stderr}'); + failingPackages.add(packageName); + print('\n\n'); + continue; + } + + final String xcodeprojListOutput = xcodeprojListResult.stdout; + Map xcodeprojListOutputJson = + jsonDecode(xcodeprojListOutput); + if (!xcodeprojListOutputJson['project']['targets'].contains(target)) { + failingPackages.add(packageName); + print('$target not configured for $packageName, test failed.'); + print( + 'Please check the scheme for the test target if it matches the name $target.\n' + 'If this plugin does not have an XCTest target, use the $_kSkip flag in the $name command to skip the plugin.'); + print('\n\n'); + continue; + } + // Found the scheme, running tests + print('Running XCTests:$target for $packageName ...'); + final List xctestArgs = [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + target, + '-destination', + destination, + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ]; + final String completeTestCommand = + '$_kXcodeBuildCommand ${xctestArgs.join(' ')}'; + print(completeTestCommand); + final int exitCode = await processRunner + .runAndStream(_kXcodeBuildCommand, xctestArgs, workingDir: example); + if (exitCode == 0) { + print('Successfully ran xctest for $packageName'); + } else { + failingPackages.add(packageName); + } + } + } + + // Command end, print reports. + if (failingPackages.isEmpty) { + print("All XCTests have passed!"); + } else { + print( + 'The following packages are failing XCTests (see above for details):'); + for (String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + } + + Future _findAvailableIphoneSimulator() async { + // Find the first available destination if not specified. + final List findSimulatorsArguments = [ + 'simctl', + 'list', + '--json' + ]; + final String findSimulatorCompleteCommand = + '$_kXCRunCommand ${findSimulatorsArguments.join(' ')}'; + print('Looking for available simulators...'); + print(findSimulatorCompleteCommand); + final io.ProcessResult findSimulatorsResult = + await processRunner.run(_kXCRunCommand, findSimulatorsArguments); + if (findSimulatorsResult.exitCode != 0) { + print('Error occurred while running "$findSimulatorCompleteCommand":\n' + '${findSimulatorsResult.stderr}'); + throw ToolExit(1); + } + final Map simulatorListJson = + jsonDecode(findSimulatorsResult.stdout); + final List runtimes = simulatorListJson['runtimes']; + final Map devices = simulatorListJson['devices']; + if (runtimes.isEmpty || devices.isEmpty) { + return null; + } + String id; + // Looking for runtimes, trying to find one with highest OS version. + for (Map runtimeMap in runtimes.reversed) { + if (!runtimeMap['name'].contains('iOS')) { + continue; + } + final String runtimeID = runtimeMap['identifier']; + final List devicesForRuntime = devices[runtimeID]; + if (devicesForRuntime.isEmpty) { + continue; + } + // Looking for runtimes, trying to find latest version of device. + for (Map device in devicesForRuntime.reversed) { + if (device['availabilityError'] != null || + (device['isAvailable'] as bool == false)) { + continue; + } + id = device['udid']; + print('device selected: $device'); + return id; + } + } + return null; + } +} diff --git a/packages/plugin_tools/pubspec.yaml b/packages/plugin_tools/pubspec.yaml new file mode 100644 index 000000000000..f8b452910236 --- /dev/null +++ b/packages/plugin_tools/pubspec.yaml @@ -0,0 +1,30 @@ +name: flutter_plugin_tools +description: Productivity utils for hosting multiple plugins within one repository. +homepage: https://github.com/flutter/plugin_tools +version: 0.0.45+1 + +dependencies: + args: "^1.4.3" + path: "^1.6.1" + http: "^0.12.1" + async: "^2.0.7" + yaml: "^2.1.15" + quiver: "^2.0.2" + pub_semver: ^1.4.2 + colorize: ^2.0.0 + git: ^1.0.0 + platform: ^2.2.0 + pubspec_parse: "^0.1.4" + test: ^1.6.4 + meta: ^1.1.7 + file: ^5.0.10 + uuid: ^2.0.4 + http_multi_server: ^2.2.0 + +dev_dependencies: + matcher: ^0.12.6 + mockito: ^4.1.1 + pedantic: 1.8.0 + +environment: + sdk: ">=2.3.0 <3.0.0" diff --git a/packages/plugin_tools/test/analyze_command_test.dart b/packages/plugin_tools/test/analyze_command_test.dart new file mode 100644 index 000000000000..9e7a42bbb680 --- /dev/null +++ b/packages/plugin_tools/test/analyze_command_test.dart @@ -0,0 +1,93 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/analyze_command.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + RecordingProcessRunner processRunner; + CommandRunner runner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final AnalyzeCommand analyzeCommand = AnalyzeCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('analyze_command', 'Test for analyze_command'); + runner.addCommand(analyzeCommand); + }); + + tearDown(() { + mockPackagesDir.deleteSync(recursive: true); + }); + + test('analyzes all packages', () async { + final Directory plugin1Dir = await createFakePlugin('a'); + final Directory plugin2Dir = await createFakePlugin('b'); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('pub', ['global', 'activate', 'tuneup'], + mockPackagesDir.path), + ProcessCall('flutter', ['packages', 'get'], plugin1Dir.path), + ProcessCall('flutter', ['packages', 'get'], plugin2Dir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + plugin1Dir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + plugin2Dir.path), + ])); + }); + + group('verifies analysis settings', () { + test('fails analysis_options.yaml', () async { + await createFakePlugin('foo', withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + await expectLater(() => runner.run(['analyze']), + throwsA(const TypeMatcher())); + }); + + test('fails .analysis_options', () async { + await createFakePlugin('foo', withExtraFiles: >[ + ['.analysis_options'] + ]); + + await expectLater(() => runner.run(['analyze']), + throwsA(const TypeMatcher())); + }); + + test('takes an allow list', () async { + final Directory pluginDir = + await createFakePlugin('foo', withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze', '--custom-analysis', 'foo']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('pub', ['global', 'activate', 'tuneup'], + mockPackagesDir.path), + ProcessCall('flutter', ['packages', 'get'], pluginDir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + pluginDir.path), + ])); + }); + }); +} diff --git a/packages/plugin_tools/test/build_examples_command_test.dart b/packages/plugin_tools/test/build_examples_command_test.dart new file mode 100644 index 000000000000..eaf5049dcc02 --- /dev/null +++ b/packages/plugin_tools/test/build_examples_command_test.dart @@ -0,0 +1,470 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/build_examples_command.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test build_example_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final BuildExamplesCommand command = BuildExamplesCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner( + 'build_examples_command', 'Test for build_example_command'); + runner.addCommand(command); + cleanupPackages(); + }); + + test('building for iOS when plugin is not set up for iOS results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ipa', '--no-macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING IPA for $packageName', + 'iOS is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for ios', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'build-examples', + '--ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING IPA for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Linux when plugin is not set up for Linux results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--linux']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Linux for $packageName', + 'Linux is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --linux with no + // Linux implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for Linux', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--linux']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Linux for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'linux'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('building for macos with no implementation results in no-op', + () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['example', 'test'], + ]); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING macOS for $packageName', + '\macOS is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + test('building for macos', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ['example', 'macos', 'macos.swift'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING macOS for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['pub', 'get'], + pluginExampleDirectory.path), + ProcessCall(flutterCommand, ['build', 'macos'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Windows when plugin is not set up for Windows results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isWindowsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--windows']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Windows for $packageName', + 'Windows is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for windows', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isWindowsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--windows']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Windows for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'windows'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Android when plugin is not set up for Android results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--apk', '--no-ipa']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING APK for $packageName', + 'Android is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for android', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'build-examples', + '--apk', + '--no-ipa', + '--no-macos', + ]); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING APK for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'apk'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('enable-experiment flag for Android', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'build-examples', + '--apk', + '--no-ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + ['build', 'apk', '--enable-experiment=exp1'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('enable-experiment flag for ios', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'build-examples', + '--ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + }); +} diff --git a/packages/plugin_tools/test/common_test.dart b/packages/plugin_tools/test/common_test.dart new file mode 100644 index 000000000000..b3504c2358d9 --- /dev/null +++ b/packages/plugin_tools/test/common_test.dart @@ -0,0 +1,100 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + RecordingProcessRunner processRunner; + CommandRunner runner; + List plugins; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + plugins = []; + final SamplePluginCommand samplePluginCommand = SamplePluginCommand( + plugins, + mockPackagesDir, + mockFileSystem, + processRunner: processRunner, + ); + runner = + CommandRunner('common_command', 'Test for common functionality'); + runner.addCommand(samplePluginCommand); + }); + + tearDown(() { + mockPackagesDir.deleteSync(recursive: true); + }); + + test('all plugins from file system', () async { + final Directory plugin1 = createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run(['sample']); + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('exclude plugins when plugins flag is specified', () async { + createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run( + ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); + expect(plugins, unorderedEquals([plugin2.path])); + }); + + test('exclude plugins when plugins flag isn\'t specified', () async { + createFakePlugin('plugin1'); + createFakePlugin('plugin2'); + await runner.run(['sample', '--exclude=plugin1,plugin2']); + expect(plugins, unorderedEquals([])); + }); + + test('exclude federated plugins when plugins flag is specified', () async { + createFakePlugin('plugin1', parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run([ + 'sample', + '--plugins=federated/plugin1,plugin2', + '--exclude=federated/plugin1' + ]); + expect(plugins, unorderedEquals([plugin2.path])); + }); + + test('exclude entire federated plugins when plugins flag is specified', + () async { + createFakePlugin('plugin1', parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run([ + 'sample', + '--plugins=federated/plugin1,plugin2', + '--exclude=federated' + ]); + expect(plugins, unorderedEquals([plugin2.path])); + }); +} + +class SamplePluginCommand extends PluginCommand { + SamplePluginCommand( + this.plugins_, + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner); + + List plugins_; + + @override + final String name = 'sample'; + + @override + final String description = 'sample command'; + + @override + Future run() async { + await for (Directory package in getPlugins()) { + this.plugins_.add(package.path); + } + } +} diff --git a/packages/plugin_tools/test/drive_examples_command_test.dart b/packages/plugin_tools/test/drive_examples_command_test.dart new file mode 100644 index 000000000000..f4bdd95c1664 --- /dev/null +++ b/packages/plugin_tools/test/drive_examples_command_test.dart @@ -0,0 +1,505 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test drive_example_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final DriveExamplesCommand command = DriveExamplesCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner( + 'drive_examples_command', 'Test for drive_example_command'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('driving under folder "test"', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], + isIosPlugin: true, + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving under folder "test_driver"', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving under folder "test_driver" when test files are missing"', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await expectLater( + () => runCapturingPrint(runner, ['drive-examples']), + throwsA(const TypeMatcher())); + }); + + test( + 'driving under folder "test_driver" when targets are under "integration_test"', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'integration_test.dart'], + ['example', 'integration_test', 'bar_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'ignore_me.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String driverTestPath = p.join('test_driver', 'integration_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'bar_test.dart'), + ], + pluginExampleDirectory.path), + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'foo_test.dart'), + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support Linux is a no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --linux on a non-Linux + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Linux plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isLinuxPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'linux', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport macOS is a no-op', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ]); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + test('driving on a macOS plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ['example', 'macos', 'macos.swift'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'macos', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport windows is a no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --windows on a non-windows + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Windows plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isWindowsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'windows', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support mobile is no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('enable-experiment flag', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], + isIosPlugin: true, + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'drive-examples', + '--enable-experiment=exp1', + ]); + + String deviceTestPath = p.join('test', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--enable-experiment=exp1', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + }); +} diff --git a/packages/plugin_tools/test/firebase_test_lab_test.dart b/packages/plugin_tools/test/firebase_test_lab_test.dart new file mode 100644 index 000000000000..97b977619d57 --- /dev/null +++ b/packages/plugin_tools/test/firebase_test_lab_test.dart @@ -0,0 +1,256 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$FirebaseTestLabCommand', () { + final List printedMessages = []; + CommandRunner runner; + RecordingProcessRunner processRunner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final FirebaseTestLabCommand command = FirebaseTestLabCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, + print: (Object message) => printedMessages.add(message.toString())); + + runner = CommandRunner( + 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); + runner.addCommand(command); + }); + + tearDown(() { + printedMessages.clear(); + }); + + test('retries gcloud set', () async { + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(1); + processRunner.processToReturn = mockProcess; + createFakePlugin('plugin', withExtraFiles: >[ + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + await expectLater( + () => runCapturingPrint(runner, ['firebase-test-lab']), + throwsA(const TypeMatcher())); + expect( + printedMessages, + contains( + "\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.")); + }); + + test('runs e2e tests', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['test', 'plugin_test.dart'], + ['test', 'plugin_e2e.dart'], + ['should_not_run_e2e.dart'], + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'should_not_run.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + ]); + + expect( + printedMessages, + orderedEquals([ + '\nRUNNING FIREBASE TEST LAB TESTS for plugin', + '\nFirebase project configured.', + '\n\n', + 'All Firebase Test Lab tests successful!', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ]), + ); + }); + + test('experimental flag', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['test', 'plugin_test.dart'], + ['test', 'plugin_e2e.dart'], + ['should_not_run_e2e.dart'], + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'should_not_run.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + + await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--test-run-id', + 'testRunId', + '--enable-experiment=exp1', + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ]), + ); + + cleanupPackages(); + }); + }); +} diff --git a/packages/plugin_tools/test/lint_podspecs_command_test.dart b/packages/plugin_tools/test/lint_podspecs_command_test.dart new file mode 100644 index 000000000000..49d6ad4d8e20 --- /dev/null +++ b/packages/plugin_tools/test/lint_podspecs_command_test.dart @@ -0,0 +1,202 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$LintPodspecsCommand', () { + CommandRunner runner; + MockPlatform mockPlatform; + final RecordingProcessRunner processRunner = RecordingProcessRunner(); + List printedMessages; + + setUp(() { + initializeFakePackages(); + + printedMessages = []; + mockPlatform = MockPlatform(); + when(mockPlatform.isMacOS).thenReturn(true); + final LintPodspecsCommand command = LintPodspecsCommand( + mockPackagesDir, + mockFileSystem, + processRunner: processRunner, + platform: mockPlatform, + print: (Object message) => printedMessages.add(message.toString()), + ); + + runner = + CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); + runner.addCommand(command); + final MockProcess mockLintProcess = MockProcess(); + mockLintProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockLintProcess; + processRunner.recordedCalls.clear(); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('only runs on macOS', () async { + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + when(mockPlatform.isMacOS).thenReturn(false); + await runner.run(['podspecs']); + + expect( + processRunner.recordedCalls, + equals([]), + ); + }); + + test('runs pod lib lint on a podspec', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['ios', 'plugin1.podspec'], + ['bogus.dart'], // Ignore non-podspecs. + ]); + + processRunner.resultStdout = 'Foo'; + processRunner.resultStderr = 'Bar'; + + await runner.run(['podspecs']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--analyze', + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--analyze', + ], + mockPackagesDir.path), + ]), + ); + + expect( + printedMessages, contains('Linting and analyzing plugin1.podspec')); + expect(printedMessages, contains('Foo')); + expect(printedMessages, contains('Bar')); + }); + + test('skips podspecs with known issues', () async { + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'] + ]); + createFakePlugin('plugin2', withExtraFiles: >[ + ['plugin2.podspec'] + ]); + + await runner + .run(['podspecs', '--skip=plugin1', '--skip=plugin2']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ]), + ); + }); + + test('skips analyzer for podspecs with known warnings', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + await runner.run(['podspecs', '--no-analyze=plugin1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + ], + mockPackagesDir.path), + ]), + ); + + expect(printedMessages, contains('Linting plugin1.podspec')); + }); + + test('allow warnings for podspecs with known warnings', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + await runner.run(['podspecs', '--ignore-warnings=plugin1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--allow-warnings', + '--analyze', + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--allow-warnings', + '--analyze', + ], + mockPackagesDir.path), + ]), + ); + + expect( + printedMessages, contains('Linting and analyzing plugin1.podspec')); + }); + }); +} + +class MockPlatform extends Mock implements Platform {} diff --git a/packages/plugin_tools/test/list_command_test.dart b/packages/plugin_tools/test/list_command_test.dart new file mode 100644 index 000000000000..478625283dd0 --- /dev/null +++ b/packages/plugin_tools/test/list_command_test.dart @@ -0,0 +1,198 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/list_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$ListCommand', () { + CommandRunner runner; + + setUp(() { + initializeFakePackages(); + final ListCommand command = ListCommand(mockPackagesDir, mockFileSystem); + + runner = CommandRunner('list_test', 'Test for $ListCommand'); + runner.addCommand(command); + }); + + test('lists plugins', () async { + createFakePlugin('plugin1'); + createFakePlugin('plugin2'); + + final List plugins = + await runCapturingPrint(runner, ['list', '--type=plugin']); + + expect( + plugins, + orderedEquals([ + '/packages/plugin1', + '/packages/plugin2', + ]), + ); + + cleanupPackages(); + }); + + test('lists examples', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=example']); + + expect( + examples, + orderedEquals([ + '/packages/plugin1/example', + '/packages/plugin2/example/example1', + '/packages/plugin2/example/example2', + ]), + ); + + cleanupPackages(); + }); + + test('lists packages', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List packages = + await runCapturingPrint(runner, ['list', '--type=package']); + + expect( + packages, + unorderedEquals([ + '/packages/plugin1', + '/packages/plugin1/example', + '/packages/plugin2', + '/packages/plugin2/example/example1', + '/packages/plugin2/example/example2', + '/packages/plugin3', + ]), + ); + + cleanupPackages(); + }); + + test('lists files', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=file']); + + expect( + examples, + unorderedEquals([ + '/packages/plugin1/pubspec.yaml', + '/packages/plugin1/example/pubspec.yaml', + '/packages/plugin2/pubspec.yaml', + '/packages/plugin2/example/example1/pubspec.yaml', + '/packages/plugin2/example/example2/pubspec.yaml', + '/packages/plugin3/pubspec.yaml', + ]), + ); + + cleanupPackages(); + }); + + test('lists plugins using federated plugin layout', () async { + createFakePlugin('plugin1'); + + // Create a federated plugin by creating a directory under the packages + // directory with several packages underneath. + final Directory federatedPlugin = + mockPackagesDir.childDirectory('my_plugin')..createSync(); + final Directory clientLibrary = + federatedPlugin.childDirectory('my_plugin')..createSync(); + createFakePubspec(clientLibrary); + final Directory webLibrary = + federatedPlugin.childDirectory('my_plugin_web')..createSync(); + createFakePubspec(webLibrary); + final Directory macLibrary = + federatedPlugin.childDirectory('my_plugin_macos')..createSync(); + createFakePubspec(macLibrary); + + // Test without specifying `--type`. + final List plugins = + await runCapturingPrint(runner, ['list']); + + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + '/packages/my_plugin/my_plugin', + '/packages/my_plugin/my_plugin_web', + '/packages/my_plugin/my_plugin_macos', + ]), + ); + + cleanupPackages(); + }); + + test('can filter plugins with the --plugins argument', () async { + createFakePlugin('plugin1'); + + // Create a federated plugin by creating a directory under the packages + // directory with several packages underneath. + final Directory federatedPlugin = + mockPackagesDir.childDirectory('my_plugin')..createSync(); + final Directory clientLibrary = + federatedPlugin.childDirectory('my_plugin')..createSync(); + createFakePubspec(clientLibrary); + final Directory webLibrary = + federatedPlugin.childDirectory('my_plugin_web')..createSync(); + createFakePubspec(webLibrary); + final Directory macLibrary = + federatedPlugin.childDirectory('my_plugin_macos')..createSync(); + createFakePubspec(macLibrary); + + List plugins = await runCapturingPrint( + runner, ['list', '--plugins=plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--plugins=my_plugin']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin', + '/packages/my_plugin/my_plugin_web', + '/packages/my_plugin/my_plugin_macos', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--plugins=my_plugin/my_plugin_web']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin_web', + ]), + ); + + plugins = await runCapturingPrint(runner, + ['list', '--plugins=my_plugin/my_plugin_web,plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + '/packages/my_plugin/my_plugin_web', + ]), + ); + }); + }); +} diff --git a/packages/plugin_tools/test/mocks.dart b/packages/plugin_tools/test/mocks.dart new file mode 100644 index 000000000000..3e17ff8efd32 --- /dev/null +++ b/packages/plugin_tools/test/mocks.dart @@ -0,0 +1,33 @@ +import 'dart:async'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:mockito/mockito.dart'; + +class MockProcess extends Mock implements io.Process { + final Completer exitCodeCompleter = Completer(); + final StreamController> stdoutController = + StreamController>(); + final StreamController> stderrController = + StreamController>(); + final MockIOSink stdinMock = MockIOSink(); + + @override + Future get exitCode => exitCodeCompleter.future; + + @override + Stream> get stdout => stdoutController.stream; + + @override + Stream> get stderr => stderrController.stream; + + @override + IOSink get stdin => stdinMock; +} + +class MockIOSink extends Mock implements IOSink { + List lines = []; + + @override + void writeln([Object obj = ""]) => lines.add(obj); +} diff --git a/packages/plugin_tools/test/publish_plugin_command_test.dart b/packages/plugin_tools/test/publish_plugin_command_test.dart new file mode 100644 index 000000000000..d01bfa67ce24 --- /dev/null +++ b/packages/plugin_tools/test/publish_plugin_command_test.dart @@ -0,0 +1,359 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:git/git.dart'; +import 'package:matcher/matcher.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + const String testPluginName = 'foo'; + final List printedMessages = []; + + Directory parentDir; + Directory pluginDir; + GitDir gitDir; + TestProcessRunner processRunner; + CommandRunner commandRunner; + MockStdin mockStdin; + + setUp(() async { + // This test uses a local file system instead of an in memory one throughout + // so that git actually works. In setup we initialize a mono repo of plugins + // with one package and commit everything to Git. + parentDir = const LocalFileSystem() + .systemTempDirectory + .createTempSync('publish_plugin_command_test-'); + initializeFakePackages(parentDir: parentDir); + pluginDir = createFakePlugin(testPluginName, withSingleExample: false); + assert(pluginDir != null && pluginDir.existsSync()); + createFakePubspec(pluginDir, includeVersion: true); + io.Process.runSync('git', ['init'], + workingDirectory: mockPackagesDir.path); + gitDir = await GitDir.fromExisting(mockPackagesDir.path); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Initial commit']); + processRunner = TestProcessRunner(); + mockStdin = MockStdin(); + commandRunner = CommandRunner('tester', '') + ..addCommand(PublishPluginCommand( + mockPackagesDir, const LocalFileSystem(), + processRunner: processRunner, + print: (Object message) => printedMessages.add(message.toString()), + stdinput: mockStdin)); + }); + + tearDown(() { + parentDir.deleteSync(recursive: true); + printedMessages.clear(); + }); + + group('Initial validation', () { + test('requires a package flag', () async { + await expectLater(() => commandRunner.run(['publish-plugin']), + throwsA(const TypeMatcher())); + + expect( + printedMessages.last, contains("Must specify a package to publish.")); + }); + + test('requires an existing flag', () async { + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', 'iamerror']), + throwsA(const TypeMatcher())); + + expect(printedMessages.last, contains('iamerror does not exist')); + }); + + test('refuses to proceed with dirty files', () async { + pluginDir.childFile('tmp').createSync(); + + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', testPluginName]), + throwsA(const TypeMatcher())); + + expect( + printedMessages.last, + contains( + "There are files in the package directory that haven't been saved in git.")); + }); + + test('fails immediately if the remote doesn\'t exist', () async { + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', testPluginName]), + throwsA(const TypeMatcher())); + + expect(processRunner.results.last.stderr, contains("No such remote")); + }); + + test("doesn't validate the remote if it's not pushing tags", () async { + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + + expect(printedMessages.last, 'Done!'); + }); + }); + + group('Publishes package', () { + test('while showing all output from pub publish to the user', () async { + final Future publishCommand = commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + processRunner.mockPublishProcess.stdoutController.add(utf8.encode('Foo')); + processRunner.mockPublishProcess.stderrController.add(utf8.encode('Bar')); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await publishCommand; + + expect(printedMessages, contains('Foo')); + expect(printedMessages, contains('Bar')); + }); + + test('forwards input from the user to `pub publish`', () async { + final Future publishCommand = commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + mockStdin.controller.add(utf8.encode('user input')); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await publishCommand; + + expect(processRunner.mockPublishProcess.stdinMock.lines, + contains('user input')); + }); + + test('forwards --pub-publish-flags to pub publish', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + '--pub-publish-flags', + '--dry-run,--server=foo' + ]); + + expect(processRunner.mockPublishArgs.length, 4); + expect(processRunner.mockPublishArgs[0], 'pub'); + expect(processRunner.mockPublishArgs[1], 'publish'); + expect(processRunner.mockPublishArgs[2], '--dry-run'); + expect(processRunner.mockPublishArgs[3], '--server=foo'); + }); + + test('throws if pub publish fails', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(128); + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains("Publish failed. Exiting.")); + }); + }); + + group('Tags release', () { + test('with the version and name from the pubspec.yaml', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ]); + + final String tag = + (await gitDir.runCommand(['show-ref', 'fake_package-v0.0.1'])) + .stdout; + expect(tag, isNotEmpty); + }); + + test('only if publishing succeeded', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(128); + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains("Publish failed. Exiting.")); + final String tag = (await gitDir.runCommand( + ['show-ref', 'fake_package-v0.0.1'], + throwOnError: false)) + .stdout; + expect(tag, isEmpty); + }); + }); + + group('Pushes tags', () { + setUp(() async { + await gitDir.runCommand( + ['remote', 'add', 'upstream', 'http://localhost:8000']); + }); + + test('requires user confirmation', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'help'; + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains('Tag push canceled.')); + }); + + test('to upstream by default', () async { + await gitDir.runCommand(['tag', 'garbage']); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + + test('to different remotes based on a flag', () async { + await gitDir.runCommand( + ['remote', 'add', 'origin', 'http://localhost:8001']); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--remote', + 'origin', + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'origin'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + + test('only if tagging and pushing to remotes are both enabled', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-tag-release', + ]); + + expect(processRunner.pushTagsArgs.isEmpty, isTrue); + expect(printedMessages.last, 'Done!'); + }); + }); +} + +class TestProcessRunner extends ProcessRunner { + final List results = []; + final MockProcess mockPublishProcess = MockProcess(); + final List mockPublishArgs = []; + final MockProcessResult mockPushTagsResult = MockProcessResult(); + final List pushTagsArgs = []; + + @override + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + // Don't ever really push tags. + if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { + pushTagsArgs.addAll(args); + return mockPushTagsResult; + } + + final io.ProcessResult result = io.Process.runSync(executable, args, + workingDirectory: workingDir?.path); + results.add(result); + if (result.exitCode != 0) { + throw ToolExit(result.exitCode); + } + return result; + } + + @override + Future start(String executable, List args, + {Directory workingDirectory}) async { + /// Never actually publish anything. Start is always and only used for this + /// since it returns something we can route stdin through. + assert(executable == 'flutter' && + args.isNotEmpty && + args[0] == 'pub' && + args[1] == 'publish'); + mockPublishArgs.addAll(args); + return mockPublishProcess; + } +} + +class MockStdin extends Mock implements io.Stdin { + final StreamController> controller = StreamController>(); + String readLineOutput; + + @override + Stream transform(StreamTransformer streamTransformer) { + return controller.stream.transform(streamTransformer); + } + + @override + StreamSubscription> listen(void onData(List event), + {Function onError, void onDone(), bool cancelOnError}) { + return controller.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + String readLineSync( + {Encoding encoding = io.systemEncoding, + bool retainNewlines = false}) => + readLineOutput; +} + +class MockProcessResult extends Mock implements io.ProcessResult {} diff --git a/packages/plugin_tools/test/test_command_test.dart b/packages/plugin_tools/test/test_command_test.dart new file mode 100644 index 000000000000..514e4c27190a --- /dev/null +++ b/packages/plugin_tools/test/test_command_test.dart @@ -0,0 +1,154 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/test_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$TestCommand', () { + CommandRunner runner; + final RecordingProcessRunner processRunner = RecordingProcessRunner(); + + setUp(() { + initializeFakePackages(); + final TestCommand command = TestCommand(mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('test_test', 'Test for $TestCommand'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + processRunner.recordedCalls.clear(); + }); + + test('runs flutter test on each plugin', () async { + final Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = + createFakePlugin('plugin2', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', ['test', '--color'], plugin1Dir.path), + ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('skips testing plugins without test directory', () async { + createFakePlugin('plugin1'); + final Directory plugin2Dir = + createFakePlugin('plugin2', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('runs pub run test on non-Flutter packages', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', + isFlutter: true, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', + isFlutter: false, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + ['test', '--color', '--enable-experiment=exp1'], + plugin1Dir.path), + ProcessCall('pub', ['get'], plugin2Dir.path), + ProcessCall( + 'pub', + ['run', '--enable-experiment=exp1', 'test'], + plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('runs on Chrome for web plugins', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ], + isFlutter: true, + isWebPlugin: true, + ); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', + ['test', '--color', '--platform=chrome'], pluginDir.path), + ]), + ); + }); + + test('enable-experiment flag', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', + isFlutter: true, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', + isFlutter: false, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + ['test', '--color', '--enable-experiment=exp1'], + plugin1Dir.path), + ProcessCall('pub', ['get'], plugin2Dir.path), + ProcessCall( + 'pub', + ['run', '--enable-experiment=exp1', 'test'], + plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + }); +} diff --git a/packages/plugin_tools/test/util.dart b/packages/plugin_tools/test/util.dart new file mode 100644 index 000000000000..ec0000d13f34 --- /dev/null +++ b/packages/plugin_tools/test/util.dart @@ -0,0 +1,291 @@ +import 'dart:async'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:platform/platform.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:quiver/collection.dart'; + +FileSystem mockFileSystem = MemoryFileSystem( + style: LocalPlatform().isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix); +Directory mockPackagesDir; + +/// Creates a mock packages directory in the mock file system. +/// +/// If [parentDir] is set the mock packages dir will be creates as a child of +/// it. If not [mockFileSystem] will be used instead. +void initializeFakePackages({Directory parentDir}) { + mockPackagesDir = + (parentDir ?? mockFileSystem.currentDirectory).childDirectory('packages'); + mockPackagesDir.createSync(); +} + +/// Creates a plugin package with the given [name] in [mockPackagesDir]. +Directory createFakePlugin( + String name, { + bool withSingleExample = false, + List withExamples = const [], + List> withExtraFiles = const >[], + bool isFlutter = true, + bool isAndroidPlugin = false, + bool isIosPlugin = false, + bool isWebPlugin = false, + bool isLinuxPlugin = false, + bool isMacOsPlugin = false, + bool isWindowsPlugin = false, + String parentDirectoryName = '', +}) { + assert(!(withSingleExample && withExamples.isNotEmpty), + 'cannot pass withSingleExample and withExamples simultaneously'); + + final Directory pluginDirectory = (parentDirectoryName != '') + ? mockPackagesDir.childDirectory(parentDirectoryName).childDirectory(name) + : mockPackagesDir.childDirectory(name); + pluginDirectory.createSync(recursive: true); + + createFakePubspec( + pluginDirectory, + name: name, + isFlutter: isFlutter, + isAndroidPlugin: isAndroidPlugin, + isIosPlugin: isIosPlugin, + isWebPlugin: isWebPlugin, + isLinuxPlugin: isLinuxPlugin, + isMacOsPlugin: isMacOsPlugin, + isWindowsPlugin: isWindowsPlugin, + ); + + if (withSingleExample) { + final Directory exampleDir = pluginDirectory.childDirectory('example') + ..createSync(); + createFakePubspec(exampleDir, + name: "${name}_example", isFlutter: isFlutter); + } else if (withExamples.isNotEmpty) { + final Directory exampleDir = pluginDirectory.childDirectory('example') + ..createSync(); + for (String example in withExamples) { + final Directory currentExample = exampleDir.childDirectory(example) + ..createSync(); + createFakePubspec(currentExample, name: example, isFlutter: isFlutter); + } + } + + for (List file in withExtraFiles) { + final List newFilePath = [pluginDirectory.path] + ..addAll(file); + final File newFile = + mockFileSystem.file(mockFileSystem.path.joinAll(newFilePath)); + newFile.createSync(recursive: true); + } + + return pluginDirectory; +} + +/// Creates a `pubspec.yaml` file with a flutter dependency. +void createFakePubspec( + Directory parent, { + String name = 'fake_package', + bool isFlutter = true, + bool includeVersion = false, + bool isAndroidPlugin = false, + bool isIosPlugin = false, + bool isWebPlugin = false, + bool isLinuxPlugin = false, + bool isMacOsPlugin = false, + bool isWindowsPlugin = false, +}) { + parent.childFile('pubspec.yaml').createSync(); + String yaml = ''' +name: $name +flutter: + plugin: + platforms: +'''; + if (isAndroidPlugin) { + yaml += ''' + android: + package: io.flutter.plugins.fake + pluginClass: FakePlugin +'''; + } + if (isIosPlugin) { + yaml += ''' + ios: + pluginClass: FLTFakePlugin +'''; + } + if (isWebPlugin) { + yaml += ''' + web: + pluginClass: FakePlugin + fileName: ${name}_web.dart +'''; + } + if (isLinuxPlugin) { + yaml += ''' + linux: + pluginClass: FakePlugin +'''; + } + if (isMacOsPlugin) { + yaml += ''' + macos: + pluginClass: FakePlugin +'''; + } + if (isWindowsPlugin) { + yaml += ''' + windows: + pluginClass: FakePlugin +'''; + } + if (isFlutter) { + yaml += ''' +dependencies: + flutter: + sdk: flutter +'''; + } + if (includeVersion) { + yaml += ''' +version: 0.0.1 +publish_to: none # Hardcoded safeguard to prevent this from somehow being published by a broken test. +'''; + } + parent.childFile('pubspec.yaml').writeAsStringSync(yaml); +} + +/// Cleans up the mock packages directory, making it an empty directory again. +void cleanupPackages() { + mockPackagesDir.listSync().forEach((FileSystemEntity entity) { + entity.deleteSync(recursive: true); + }); +} + +/// Run the command [runner] with the given [args] and return +/// what was printed. +Future> runCapturingPrint( + CommandRunner runner, List args) async { + final List prints = []; + final ZoneSpecification spec = ZoneSpecification( + print: (_, __, ___, String message) { + prints.add(message); + }, + ); + await Zone.current + .fork(specification: spec) + .run>(() => runner.run(args)); + + return prints; +} + +/// A mock [ProcessRunner] which records process calls. +class RecordingProcessRunner extends ProcessRunner { + io.Process processToReturn; + final List recordedCalls = []; + + /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. + String resultStdout; + + /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. + String resultStderr; + + @override + Future runAndStream( + String executable, + List args, { + Directory workingDir, + bool exitOnError = false, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + return Future.value( + processToReturn == null ? 0 : await processToReturn.exitCode); + } + + /// Returns [io.ProcessResult] created from [processToReturn], [resultStdout], and [resultStderr]. + @override + Future run(String executable, List args, + {Directory workingDir, + bool exitOnError = false, + stdoutEncoding = io.systemEncoding, + stderrEncoding = io.systemEncoding}) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + io.ProcessResult result; + + if (processToReturn != null) { + result = io.ProcessResult( + processToReturn.pid, + await processToReturn.exitCode, + resultStdout ?? processToReturn.stdout, + resultStderr ?? processToReturn.stderr); + } + return Future.value(result); + } + + @override + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + io.ProcessResult result; + if (processToReturn != null) { + result = io.ProcessResult( + processToReturn.pid, + await processToReturn.exitCode, + resultStdout ?? processToReturn.stdout, + resultStderr ?? processToReturn.stderr); + } + return Future.value(result); + } + + @override + Future start(String executable, List args, + {Directory workingDirectory}) async { + recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); + return Future.value(processToReturn); + } +} + +/// A recorded process call. +class ProcessCall { + const ProcessCall(this.executable, this.args, this.workingDir); + + /// The executable that was called. + final String executable; + + /// The arguments passed to [executable] in the call. + final List args; + + /// The working directory this process was called from. + final String workingDir; + + @override + bool operator ==(dynamic other) { + if (other is! ProcessCall) { + return false; + } + final ProcessCall otherCall = other; + return executable == otherCall.executable && + listsEqual(args, otherCall.args) && + workingDir == otherCall.workingDir; + } + + @override + int get hashCode => + executable?.hashCode ?? + 0 ^ args?.hashCode ?? + 0 ^ workingDir?.hashCode ?? + 0; + + @override + String toString() { + final List command = [executable]..addAll(args); + return '"${command.join(' ')}" in $workingDir'; + } +} diff --git a/packages/plugin_tools/test/version_check_test.dart b/packages/plugin_tools/test/version_check_test.dart new file mode 100644 index 000000000000..b9ace3811bff --- /dev/null +++ b/packages/plugin_tools/test/version_check_test.dart @@ -0,0 +1,319 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:git/git.dart'; +import 'package:mockito/mockito.dart'; +import "package:test/test.dart"; +import "package:flutter_plugin_tools/src/version_check_command.dart"; +import 'package:pub_semver/pub_semver.dart'; +import 'util.dart'; + +void testAllowedVersion( + String masterVersion, + String headVersion, { + bool allowed = true, + NextVersionType nextVersionType, +}) { + final Version master = Version.parse(masterVersion); + final Version head = Version.parse(headVersion); + final Map allowedVersions = + getAllowedNextVersions(master, head); + if (allowed) { + expect(allowedVersions, contains(head)); + if (nextVersionType != null) { + expect(allowedVersions[head], equals(nextVersionType)); + } + } else { + expect(allowedVersions, isNot(contains(head))); + } +} + +class MockGitDir extends Mock implements GitDir {} + +class MockProcessResult extends Mock implements ProcessResult {} + +void main() { + group('$VersionCheckCommand', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + List> gitDirCommands; + String gitDiffResponse; + Map gitShowResponses; + + setUp(() { + gitDirCommands = >[]; + gitDiffResponse = ''; + gitShowResponses = {}; + final MockGitDir gitDir = MockGitDir(); + when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { + gitDirCommands.add(invocation.positionalArguments[0]); + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'diff') { + when(mockProcessResult.stdout).thenReturn(gitDiffResponse); + } else if (invocation.positionalArguments[0][0] == 'show') { + final String response = + gitShowResponses[invocation.positionalArguments[0][1]]; + when(mockProcessResult.stdout).thenReturn(response); + } + return Future.value(mockProcessResult); + }); + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final VersionCheckCommand command = VersionCheckCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, gitDir: gitDir); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('allows valid version', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect(gitDirCommands[1].join(' '), + equals('show master:packages/plugin/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin/pubspec.yaml')); + }); + + test('denies invalid version', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', + }; + final Future> result = runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect(gitDirCommands[1].join(' '), + equals('show master:packages/plugin/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin/pubspec.yaml')); + }); + + test('gracefully handles missing pubspec.yaml', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + mockFileSystem.currentDirectory + .childDirectory('packages') + .childDirectory('plugin') + .childFile('pubspec.yaml') + .deleteSync(); + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(1)); + expect(gitDirCommands.first.join(' '), + equals('diff --name-only master HEAD')); + }); + + test('allows minor changes to platform interfaces', () async { + createFakePlugin('plugin_platform_interface'); + gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.1.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect( + gitDirCommands[1].join(' '), + equals( + 'show master:packages/plugin_platform_interface/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); + }); + + test('disallows breaking changes to platform interfaces', () async { + createFakePlugin('plugin_platform_interface'); + gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml': + 'version: 2.0.0', + }; + final Future> output = runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + await expectLater( + output, + throwsA(const TypeMatcher()), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect( + gitDirCommands[1].join(' '), + equals( + 'show master:packages/plugin_platform_interface/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); + }); + }); + + group("Pre 1.0", () { + test("nextVersion allows patch version", () { + testAllowedVersion("0.12.0", "0.12.0+1", + nextVersionType: NextVersionType.PATCH); + testAllowedVersion("0.12.0+4", "0.12.0+5", + nextVersionType: NextVersionType.PATCH); + }); + + test("nextVersion does not allow jumping patch", () { + testAllowedVersion("0.12.0", "0.12.0+2", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.0+4", allowed: false); + }); + + test("nextVersion does not allow going back", () { + testAllowedVersion("0.12.0", "0.11.0", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.0+1", allowed: false); + testAllowedVersion("0.12.0+1", "0.12.0", allowed: false); + }); + + test("nextVersion allows minor version", () { + testAllowedVersion("0.12.0", "0.12.1", + nextVersionType: NextVersionType.MINOR); + testAllowedVersion("0.12.0+4", "0.12.1", + nextVersionType: NextVersionType.MINOR); + }); + + test("nextVersion does not allow jumping minor", () { + testAllowedVersion("0.12.0", "0.12.2", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.3", allowed: false); + }); + }); + + group("Releasing 1.0", () { + test("nextVersion allows releasing 1.0", () { + testAllowedVersion("0.12.0", "1.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion("0.12.0+4", "1.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test("nextVersion does not allow jumping major", () { + testAllowedVersion("0.12.0", "2.0.0", allowed: false); + testAllowedVersion("0.12.0+4", "2.0.0", allowed: false); + }); + + test("nextVersion does not allow un-releasing", () { + testAllowedVersion("1.0.0", "0.12.0+4", allowed: false); + testAllowedVersion("1.0.0", "0.12.0", allowed: false); + }); + }); + + group("Post 1.0", () { + test("nextVersion allows patch jumps", () { + testAllowedVersion("1.0.1", "1.0.2", + nextVersionType: NextVersionType.PATCH); + testAllowedVersion("1.0.0", "1.0.1", + nextVersionType: NextVersionType.PATCH); + }); + + test("nextVersion does not allow build jumps", () { + testAllowedVersion("1.0.1", "1.0.1+1", allowed: false); + testAllowedVersion("1.0.0+5", "1.0.0+6", allowed: false); + }); + + test("nextVersion does not allow skipping patches", () { + testAllowedVersion("1.0.1", "1.0.3", allowed: false); + testAllowedVersion("1.0.0", "1.0.6", allowed: false); + }); + + test("nextVersion allows minor version jumps", () { + testAllowedVersion("1.0.1", "1.1.0", + nextVersionType: NextVersionType.MINOR); + testAllowedVersion("1.0.0", "1.1.0", + nextVersionType: NextVersionType.MINOR); + }); + + test("nextVersion does not allow skipping minor versions", () { + testAllowedVersion("1.0.1", "1.2.0", allowed: false); + testAllowedVersion("1.1.0", "1.3.0", allowed: false); + }); + + test("nextVersion allows breaking changes", () { + testAllowedVersion("1.0.1", "2.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion("1.0.0", "2.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test("nextVersion allows null safety pre prelease", () { + testAllowedVersion("1.0.1", "2.0.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0", "2.0.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0-nullsafety", "1.0.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0-nullsafety.1", "1.0.0-nullsafety.2", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0", "0.2.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0-nullsafety", "0.1.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0-nullsafety.1", "0.1.0-nullsafety.2", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0", "1.1.0-nullsafety", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.1.0-nullsafety", "1.1.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0", "0.1.1-nullsafety", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.1-nullsafety", "0.1.1-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + }); + + test("nextVersion does not allow skipping major versions", () { + testAllowedVersion("1.0.1", "3.0.0", allowed: false); + testAllowedVersion("1.1.0", "2.3.0", allowed: false); + }); + }); +} diff --git a/packages/plugin_tools/test/xctest_command_test.dart b/packages/plugin_tools/test/xctest_command_test.dart new file mode 100644 index 000000000000..007c2e12188c --- /dev/null +++ b/packages/plugin_tools/test/xctest_command_test.dart @@ -0,0 +1,358 @@ +// 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 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/xctest_command.dart'; +import 'package:test/test.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +final _kDeviceListMap = { + "runtimes": [ + { + "bundlePath": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime", + "buildversion": "17A577", + "runtimeRoot": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-0", + "version": "13.0", + "isAvailable": true, + "name": "iOS 13.0" + }, + { + "bundlePath": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime", + "buildversion": "17L255", + "runtimeRoot": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-4", + "version": "13.4", + "isAvailable": true, + "name": "iOS 13.4" + }, + { + "bundlePath": + "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime", + "buildversion": "17T531", + "runtimeRoot": + "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.watchOS-6-2", + "version": "6.2.1", + "isAvailable": true, + "name": "watchOS 6.2" + } + ], + "devices": { + "com.apple.CoreSimulator.SimRuntime.iOS-13-4": [ + { + "dataPath": + "/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data", + "logPath": + "/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774", + "udid": "2706BBEB-1E01-403E-A8E9-70E8E5A24774", + "isAvailable": true, + "deviceTypeIdentifier": + "com.apple.CoreSimulator.SimDeviceType.iPhone-8", + "state": "Shutdown", + "name": "iPhone 8" + }, + { + "dataPath": + "/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data", + "logPath": + "/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A", + "udid": "1E76A0FD-38AC-4537-A989-EA639D7D012A", + "isAvailable": true, + "deviceTypeIdentifier": + "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus", + "state": "Shutdown", + "name": "iPhone 8 Plus" + } + ] + } +}; + +void main() { + const String _kDestination = '--ios-destination'; + const String _kTarget = '--target'; + const String _kSkip = '--skip'; + + group('test xctest_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final XCTestCommand command = XCTestCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('xctest_command', 'Test for xctest_command'); + runner.addCommand(command); + cleanupPackages(); + }); + + test('Not specifying --target throws', () async { + await expectLater( + () => runner.run(['xctest', _kDestination, 'a_destination']), + throwsA(const TypeMatcher())); + }); + + test('skip if ios is not supported', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + expect(output, contains('iOS is not supported by this plugin.')); + expect(processRunner.recordedCalls, orderedEquals([])); + + cleanupPackages(); + }); + + test('running with correct scheme and destination, did not find scheme', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = '{"project":{"targets":["bar_scheme"]}}'; + + await expectLater(() async { + final List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + expect(output, + contains('foo_scheme not configured for plugin, test failed.')); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('xcrun', ['simctl', 'list', '--json'], null), + ProcessCall( + 'xcodebuild', + [ + '-project', + 'ios/Runner.xcodeproj', + '-list', + '-json' + ], + pluginExampleDirectory.path), + ])); + }, throwsA(const TypeMatcher())); + cleanupPackages(); + }); + + test('running with correct scheme and destination, found scheme', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + + expect(output, contains('Successfully ran xctest for plugin')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'foo_destination', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory.path), + ])); + + cleanupPackages(); + }); + + test('running with correct scheme and destination, skip 1 plugin', + () async { + createFakePlugin('plugin1', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + createFakePlugin('plugin2', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory1 = + mockPackagesDir.childDirectory('plugin1').childDirectory('example'); + createFakePubspec(pluginExampleDirectory1, isFlutter: true); + final Directory pluginExampleDirectory2 = + mockPackagesDir.childDirectory('plugin2').childDirectory('example'); + createFakePubspec(pluginExampleDirectory2, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination', + _kSkip, + 'plugin1' + ]); + + expect(output, contains('plugin1 was skipped with the --skip flag.')); + expect(output, contains('Successfully ran xctest for plugin2')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory2.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'foo_destination', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory2.path), + ])); + + cleanupPackages(); + }); + + test('Not specifying --ios-destination assigns an available simulator', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final Map schemeCommandResult = { + "project": { + "targets": ["bar_scheme", "foo_scheme"] + } + }; + // For simplicity of the test, we combine all the mock results into a single mock result, each internal command + // will get this result and they should still be able to parse them correctly. + processRunner.resultStdout = + jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); + await runner.run([ + 'xctest', + _kTarget, + 'foo_scheme', + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('xcrun', ['simctl', 'list', '--json'], null), + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory.path), + ])); + + cleanupPackages(); + }); + }); +} diff --git a/plugin_tools b/plugin_tools deleted file mode 160000 index ce5cae0d800a..000000000000 --- a/plugin_tools +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ce5cae0d800aa9100e902fc0a5103ef350db0c1d From 16c39df0473052e16a6e3bef5f0b4895a29b61e6 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 15:49:19 -0800 Subject: [PATCH 12/27] typo --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index 6dafca5fb435..b1426e10a2be 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -146,7 +146,7 @@ task: - flutter channel master - flutter upgrade - git fetch origin master - activate_script: pub global activate -s path ./package/plugin_tools + activate_script: pub global activate -s path ./packages/plugin_tools create_simulator_script: - xcrun simctl list - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-3 | xargs xcrun simctl boot From 454933cee3f1e089cfbd07a058bba513338011be Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 16:41:20 -0800 Subject: [PATCH 13/27] remove git clean --- .cirrus.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index b1426e10a2be..cba4c743b414 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -191,7 +191,6 @@ task: # https://github.com/flutter/flutter/issues/42864 - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - flutter channel $CHANNEL - - git clean -dfxx - ./script/incremental_build.sh build-examples --ipa - ./script/incremental_build.sh drive-examples - ./script/incremental_build.sh xctest --target RunnerUITests --skip $PLUGINS_TO_SKIP_XCTESTS From d8cc4ea3087098bea4abdc8f3d8dd6a392c39fda Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 17:12:43 -0800 Subject: [PATCH 14/27] rename folder to pass ci --- packages/flutter_plugin_tools/CHANGELOG.md | 288 ++++++++++ packages/flutter_plugin_tools/LICENSE | 29 + packages/flutter_plugin_tools/README.md | 45 ++ .../analysis_options.yaml | 1 + .../bin/flutter_plugin_tools.dart | 5 + .../lib/src/analyze_command.dart | 94 ++++ .../lib/src/build_examples_command.dart | 191 +++++++ .../flutter_plugin_tools/lib/src/common.dart | 466 ++++++++++++++++ .../src/create_all_plugins_app_command.dart | 200 +++++++ .../lib/src/drive_examples_command.dart | 210 ++++++++ .../lib/src/firebase_test_lab_command.dart | 264 +++++++++ .../lib/src/format_command.dart | 147 +++++ .../lib/src/java_test_command.dart | 89 +++ .../lib/src/lint_podspecs_command.dart | 146 +++++ .../lib/src/list_command.dart | 60 +++ .../flutter_plugin_tools/lib/src/main.dart | 63 +++ .../lib/src/publish_plugin_command.dart | 227 ++++++++ .../lib/src/test_command.dart | 101 ++++ .../lib/src/version_check_command.dart | 220 ++++++++ .../lib/src/xctest_command.dart | 216 ++++++++ packages/flutter_plugin_tools/pubspec.yaml | 30 ++ .../test/analyze_command_test.dart | 93 ++++ .../test/build_examples_command_test.dart | 470 ++++++++++++++++ .../test/common_test.dart | 100 ++++ .../test/drive_examples_command_test.dart | 505 ++++++++++++++++++ .../test/firebase_test_lab_test.dart | 256 +++++++++ .../test/lint_podspecs_command_test.dart | 202 +++++++ .../test/list_command_test.dart | 198 +++++++ packages/flutter_plugin_tools/test/mocks.dart | 33 ++ .../test/publish_plugin_command_test.dart | 359 +++++++++++++ .../test/test_command_test.dart | 154 ++++++ packages/flutter_plugin_tools/test/util.dart | 291 ++++++++++ .../test/version_check_test.dart | 319 +++++++++++ .../test/xctest_command_test.dart | 358 +++++++++++++ packages/fluttter_plugin_tools/CHANGELOG.md | 288 ++++++++++ packages/fluttter_plugin_tools/LICENSE | 29 + packages/fluttter_plugin_tools/README.md | 45 ++ .../analysis_options.yaml | 1 + .../bin/flutter_plugin_tools.dart | 5 + .../lib/src/analyze_command.dart | 94 ++++ .../lib/src/build_examples_command.dart | 191 +++++++ .../fluttter_plugin_tools/lib/src/common.dart | 466 ++++++++++++++++ .../src/create_all_plugins_app_command.dart | 200 +++++++ .../lib/src/drive_examples_command.dart | 210 ++++++++ .../lib/src/firebase_test_lab_command.dart | 264 +++++++++ .../lib/src/format_command.dart | 147 +++++ .../lib/src/java_test_command.dart | 89 +++ .../lib/src/lint_podspecs_command.dart | 146 +++++ .../lib/src/list_command.dart | 60 +++ .../fluttter_plugin_tools/lib/src/main.dart | 63 +++ .../lib/src/publish_plugin_command.dart | 227 ++++++++ .../lib/src/test_command.dart | 101 ++++ .../lib/src/version_check_command.dart | 220 ++++++++ .../lib/src/xctest_command.dart | 216 ++++++++ packages/fluttter_plugin_tools/pubspec.yaml | 30 ++ .../test/analyze_command_test.dart | 93 ++++ .../test/build_examples_command_test.dart | 470 ++++++++++++++++ .../test/common_test.dart | 100 ++++ .../test/drive_examples_command_test.dart | 505 ++++++++++++++++++ .../test/firebase_test_lab_test.dart | 256 +++++++++ .../test/lint_podspecs_command_test.dart | 202 +++++++ .../test/list_command_test.dart | 198 +++++++ .../fluttter_plugin_tools/test/mocks.dart | 33 ++ .../test/publish_plugin_command_test.dart | 359 +++++++++++++ .../test/test_command_test.dart | 154 ++++++ packages/fluttter_plugin_tools/test/util.dart | 291 ++++++++++ .../test/version_check_test.dart | 319 +++++++++++ .../test/xctest_command_test.dart | 358 +++++++++++++ 68 files changed, 12860 insertions(+) create mode 100644 packages/flutter_plugin_tools/CHANGELOG.md create mode 100644 packages/flutter_plugin_tools/LICENSE create mode 100644 packages/flutter_plugin_tools/README.md create mode 100644 packages/flutter_plugin_tools/analysis_options.yaml create mode 100644 packages/flutter_plugin_tools/bin/flutter_plugin_tools.dart create mode 100644 packages/flutter_plugin_tools/lib/src/analyze_command.dart create mode 100644 packages/flutter_plugin_tools/lib/src/build_examples_command.dart create mode 100644 packages/flutter_plugin_tools/lib/src/common.dart create mode 100644 packages/flutter_plugin_tools/lib/src/create_all_plugins_app_command.dart create mode 100644 packages/flutter_plugin_tools/lib/src/drive_examples_command.dart create mode 100644 packages/flutter_plugin_tools/lib/src/firebase_test_lab_command.dart create mode 100644 packages/flutter_plugin_tools/lib/src/format_command.dart create mode 100644 packages/flutter_plugin_tools/lib/src/java_test_command.dart create mode 100644 packages/flutter_plugin_tools/lib/src/lint_podspecs_command.dart create mode 100644 packages/flutter_plugin_tools/lib/src/list_command.dart create mode 100644 packages/flutter_plugin_tools/lib/src/main.dart create mode 100644 packages/flutter_plugin_tools/lib/src/publish_plugin_command.dart create mode 100644 packages/flutter_plugin_tools/lib/src/test_command.dart create mode 100644 packages/flutter_plugin_tools/lib/src/version_check_command.dart create mode 100644 packages/flutter_plugin_tools/lib/src/xctest_command.dart create mode 100644 packages/flutter_plugin_tools/pubspec.yaml create mode 100644 packages/flutter_plugin_tools/test/analyze_command_test.dart create mode 100644 packages/flutter_plugin_tools/test/build_examples_command_test.dart create mode 100644 packages/flutter_plugin_tools/test/common_test.dart create mode 100644 packages/flutter_plugin_tools/test/drive_examples_command_test.dart create mode 100644 packages/flutter_plugin_tools/test/firebase_test_lab_test.dart create mode 100644 packages/flutter_plugin_tools/test/lint_podspecs_command_test.dart create mode 100644 packages/flutter_plugin_tools/test/list_command_test.dart create mode 100644 packages/flutter_plugin_tools/test/mocks.dart create mode 100644 packages/flutter_plugin_tools/test/publish_plugin_command_test.dart create mode 100644 packages/flutter_plugin_tools/test/test_command_test.dart create mode 100644 packages/flutter_plugin_tools/test/util.dart create mode 100644 packages/flutter_plugin_tools/test/version_check_test.dart create mode 100644 packages/flutter_plugin_tools/test/xctest_command_test.dart create mode 100644 packages/fluttter_plugin_tools/CHANGELOG.md create mode 100644 packages/fluttter_plugin_tools/LICENSE create mode 100644 packages/fluttter_plugin_tools/README.md create mode 100644 packages/fluttter_plugin_tools/analysis_options.yaml create mode 100644 packages/fluttter_plugin_tools/bin/flutter_plugin_tools.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/analyze_command.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/build_examples_command.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/common.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/create_all_plugins_app_command.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/drive_examples_command.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/firebase_test_lab_command.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/format_command.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/java_test_command.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/lint_podspecs_command.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/list_command.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/main.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/publish_plugin_command.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/test_command.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/version_check_command.dart create mode 100644 packages/fluttter_plugin_tools/lib/src/xctest_command.dart create mode 100644 packages/fluttter_plugin_tools/pubspec.yaml create mode 100644 packages/fluttter_plugin_tools/test/analyze_command_test.dart create mode 100644 packages/fluttter_plugin_tools/test/build_examples_command_test.dart create mode 100644 packages/fluttter_plugin_tools/test/common_test.dart create mode 100644 packages/fluttter_plugin_tools/test/drive_examples_command_test.dart create mode 100644 packages/fluttter_plugin_tools/test/firebase_test_lab_test.dart create mode 100644 packages/fluttter_plugin_tools/test/lint_podspecs_command_test.dart create mode 100644 packages/fluttter_plugin_tools/test/list_command_test.dart create mode 100644 packages/fluttter_plugin_tools/test/mocks.dart create mode 100644 packages/fluttter_plugin_tools/test/publish_plugin_command_test.dart create mode 100644 packages/fluttter_plugin_tools/test/test_command_test.dart create mode 100644 packages/fluttter_plugin_tools/test/util.dart create mode 100644 packages/fluttter_plugin_tools/test/version_check_test.dart create mode 100644 packages/fluttter_plugin_tools/test/xctest_command_test.dart diff --git a/packages/flutter_plugin_tools/CHANGELOG.md b/packages/flutter_plugin_tools/CHANGELOG.md new file mode 100644 index 000000000000..25dce424eee4 --- /dev/null +++ b/packages/flutter_plugin_tools/CHANGELOG.md @@ -0,0 +1,288 @@ +## v.0.0.45+1 + +- Don't call `flutter format` if there are no Dart files to format. + +## v.0.0.45 + +- Add exclude flag to exclude any plugin from further processing. + +## v.0.0.44+7 + +- `all-plugins-app` doesn't override the AGP version. + +## v.0.0.44+6 + +- Fix code formatting. + +## v.0.0.44+5 + +- Remove `-v` flag on drive-examples. + +## v.0.0.44+4 + +- Fix bug where directory isn't passed + +## v.0.0.44+3 + +- More verbose logging + +## v.0.0.44+2 + +- Remove pre-alpha Windows workaround to create examples on the fly. + +## v.0.0.44+1 + +- Print packages that passed tests in `xctest` command. +- Remove printing the whole list of simulators. + +## v.0.0.44 + +- Add 'xctest' command to run xctests. + +## v.0.0.43 + +- Allow minor `*-nullsafety` pre release packages. + +## v.0.0.42+1 + +- Fix test command when `--enable-experiment` is called. + +## v.0.0.42 + +- Allow `*-nullsafety` pre release packages. + +## v.0.0.41 + +- Support `--enable-experiment` flag in subcommands `test`, `build-examples`, `drive-examples`, +and `firebase-test-lab`. + +## v.0.0.40 + +- Support `integration_test/` directory for `drive-examples` command + +## v.0.0.39 + +- Support `integration_test/` directory for `package:integration_test` + +## v.0.0.38 + +- Add C++ and ObjC++ to clang-format. + +## v.0.0.37+2 + +- Make `http` and `http_multi_server` dependency version constraint more flexible. + +## v.0.0.37+1 + +- All_plugin test puts the plugin dependencies into dependency_overrides. + +## v.0.0.37 + +- Only builds mobile example apps when necessary. + +## v.0.0.36+3 + +- Add support for Linux plugins. + +## v.0.0.36+2 + +- Default to showing podspec lint warnings + +## v.0.0.36+1 + +- Serialize linting podspecs. + +## v.0.0.36 + +- Remove retry on Firebase Test Lab's call to gcloud set. +- Remove quiet flag from Firebase Test Lab's gcloud set command. +- Allow Firebase Test Lab command to continue past gcloud set network failures. + This is a mitigation for the network service sometimes not responding, + but it isn't actually necessary to have a network connection for this command. + +## v.0.0.35+1 + +- Minor cleanup to the analyze test. + +## v.0.0.35 + +- Firebase Test Lab command generates a configurable unique path suffix for results. + +## v.0.0.34 + +- Firebase Test Lab command now only tries to configure the project once +- Firebase Test Lab command now retries project configuration up to five times. + +## v.0.0.33+1 + +- Fixes formatting issues that got past our CI due to + https://github.com/flutter/flutter/issues/51585. +- Changes the default package name for testing method `createFakePubspec` back + its previous behavior. + +## v.0.0.33 + +- Version check command now fails on breaking changes to platform interfaces. +- Updated version check test to be more flexible. + +## v.0.0.32+7 + +- Ensure that Firebase Test Lab tests have a unique storage bucket for each test run. + +## v.0.0.32+6 + +- Ensure that Firebase Test Lab tests have a unique storage bucket for each package. + +## v.0.0.32+5 + +- Remove --fail-fast and --silent from lint podspec command. + +## v.0.0.32+4 + +- Update `publish-plugin` to use `flutter pub publish` instead of just `pub + publish`. Enforces a `pub publish` command that matches the Dart SDK in the + user's Flutter install. + +## v.0.0.32+3 + +- Update Firebase Testlab deprecated test device. (Pixel 3 API 28 -> Pixel 4 API 29). + +## v.0.0.32+2 + +- Runs pub get before building macos to avoid failures. + +## v.0.0.32+1 + +- Default macOS example builds to false. Previously they were running whenever + CI was itself running on macOS. + +## v.0.0.32 + +- `analyze` now asserts that the global `analysis_options.yaml` is the only one + by default. Individual directories can be excluded from this check with the + new `--custom-analysis` flag. + +## v.0.0.31+1 + +- Add --skip and --no-analyze flags to podspec command. + +## v.0.0.31 + +- Add support for macos on `DriveExamplesCommand` and `BuildExamplesCommand`. + +## v.0.0.30 + +- Adopt pedantic analysis options, fix firebase_test_lab_test. + +## v.0.0.29 + +- Add a command to run pod lib lint on podspec files. + +## v.0.0.28 + +- Increase Firebase test lab timeouts to 5 minutes. + +## v.0.0.27 + +- Run tests with `--platform=chrome` for web plugins. + +## v.0.0.26 + +- Add a command for publishing plugins to pub. + +## v.0.0.25 + +- Update `DriveExamplesCommand` to use `ProcessRunner`. +- Make `DriveExamplesCommand` rely on `ProcessRunner` to determine if the test fails or not. +- Add simple tests for `DriveExamplesCommand`. + +## v.0.0.24 + +- Gracefully handle pubspec.yaml files for new plugins. +- Additional unit testing. + +## v.0.0.23 + +- Add a test case for transitive dependency solving in the + `create_all_plugins_app` command. + +## v.0.0.22 + +- Updated firebase-test-lab command with updated conventions for test locations. +- Updated firebase-test-lab to add an optional "device" argument. +- Updated version-check command to always compare refs instead of using the working copy. +- Added unit tests for the firebase-test-lab and version-check commands. +- Add ProcessRunner to mock running processes for testing. + +## v.0.0.21 + +- Support the `--plugins` argument for federated plugins. + +## v.0.0.20 + +- Support for finding federated plugins, where one directory contains + multiple packages for different platform implementations. + +## v.0.0.19+3 + +- Use `package:file` for file I/O. + +## v.0.0.19+2 + +- Use java as language when calling `flutter create`. + +## v.0.0.19+1 + +- Rename command for `CreateAllPluginsAppCommand`. + +## v.0.0.19 + +- Use flutter create to build app testing plugin compilation. + +## v.0.0.18+2 + +- Fix `.travis.yml` file name in `README.md`. + +## v0.0.18+1 + +- Skip version check if it contains `publish_to: none`. + +## v0.0.18 + +- Add option to exclude packages from generated pubspec command. + +## v0.0.17+4 + +- Avoid trying to version-check pubspecs that are missing a version. + +## v0.0.17+3 + +- version-check accounts for [pre-1.0 patch versions](https://github.com/flutter/flutter/issues/35412). + +## v0.0.17+2 + +- Fix exception handling for version checker + +## v0.0.17+1 + +- Fix bug where we used a flag instead of an option + +## v0.0.17 + +- Add a command for checking the version number + +## v0.0.16 + +- Add a command for generating `pubspec.yaml` for All Plugins app. + +## v0.0.15 + +- Add a command for running driver tests of plugin examples. + +## v0.0.14 + +- Check for dependencies->flutter instead of top level flutter node. + +## v0.0.13 + +- Differentiate between Flutter and non-Flutter (but potentially Flutter consumed) Dart packages. diff --git a/packages/flutter_plugin_tools/LICENSE b/packages/flutter_plugin_tools/LICENSE new file mode 100644 index 000000000000..5f015bc7b321 --- /dev/null +++ b/packages/flutter_plugin_tools/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2017, Flutter +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/flutter_plugin_tools/README.md b/packages/flutter_plugin_tools/README.md new file mode 100644 index 000000000000..efdc668e81a0 --- /dev/null +++ b/packages/flutter_plugin_tools/README.md @@ -0,0 +1,45 @@ +# Flutter Plugin Tools + +[![Build Status](https://travis-ci.org/flutter/plugin_tools.svg?branch=master)](https://travis-ci.org/flutter/plugin_tools) +[![pub package](https://img.shields.io/pub/v/flutter_plugin_tools.svg)](https://pub.dartlang.org/packages/flutter_plugin_tools) + + +Flutter Plugin Tools implements a CLI with various productivity tools for hosting multiple Flutter plugins in one github +repository. It is mainly used by the [flutter/plugins](https://github.com/flutter/plugins) and +[flutter/flutterfire](https://github.com/flutter/flutterfire) repositories. It was mainly written to facilitate +testing on Travis for these repositories (see [.travis.yml](https://github.com/flutter/plugins/blob/master/.travis.yml)). + +As an example, Flutter Plugin Tools allows you to: + +* Build all plugin example apps with one command +* Run the tests of all plugins with one command +* Format all Dart, Java, Objective-C, and C++ code in the repository +* Define shards of the above tasks + +## Installation + +In order to use the tools you need to enable them once by running the following command: + +```shell +$ pub global activate flutter_plugin_tools +``` + +## Requirements + +To use all features of `flutter_plugin_tools` you'll need the following commands in your `PATH`: +* `flutter` +* `git` +* `pub` (recommended: version from `/bin/cache/dart-sdk/bin`) +* `clang-format` version 5 (alternatively, you can provide the path via `--clang-format=`) +* [`pod`](https://guides.cocoapods.org/using/getting-started.html#installation) (macOS only) + +## Usage + +```shell +$ pub global run flutter_plugin_tools +$ pub global run flutter_plugin_tools --shardIndex 0 --shardCount 3 +``` + +Run commands from the `flutter/plugins` directory. Replace `` with `help` to print a list of available commands. +The sharded example above divides the plugins into three shards +and executes the tool on the first shard (index 0). diff --git a/packages/flutter_plugin_tools/analysis_options.yaml b/packages/flutter_plugin_tools/analysis_options.yaml new file mode 100644 index 000000000000..84a5e26f95de --- /dev/null +++ b/packages/flutter_plugin_tools/analysis_options.yaml @@ -0,0 +1 @@ +include: package:pedantic/analysis_options.1.8.0.yaml diff --git a/packages/flutter_plugin_tools/bin/flutter_plugin_tools.dart b/packages/flutter_plugin_tools/bin/flutter_plugin_tools.dart new file mode 100644 index 000000000000..43edcbfe2c68 --- /dev/null +++ b/packages/flutter_plugin_tools/bin/flutter_plugin_tools.dart @@ -0,0 +1,5 @@ +// 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. + +export 'package:flutter_plugin_tools/src/main.dart'; diff --git a/packages/flutter_plugin_tools/lib/src/analyze_command.dart b/packages/flutter_plugin_tools/lib/src/analyze_command.dart new file mode 100644 index 000000000000..8cd57fa0b338 --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/analyze_command.dart @@ -0,0 +1,94 @@ +// 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:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class AnalyzeCommand extends PluginCommand { + AnalyzeCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addMultiOption(_customAnalysisFlag, + help: + 'Directories (comma seperated) that are allowed to have their own analysis options.', + defaultsTo: []); + } + + static const String _customAnalysisFlag = 'custom-analysis'; + + @override + final String name = 'analyze'; + + @override + final String description = 'Analyzes all packages using package:tuneup.\n\n' + 'This command requires "pub" and "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + + print('Verifying analysis settings...'); + final List files = packagesDir.listSync(recursive: true); + for (final FileSystemEntity file in files) { + if (file.basename != 'analysis_options.yaml' && + file.basename != '.analysis_options') { + continue; + } + + final bool whitelisted = argResults[_customAnalysisFlag].any( + (String directory) => + p.isWithin(p.join(packagesDir.path, directory), file.path)); + if (whitelisted) { + continue; + } + + print('Found an extra analysis_options.yaml in ${file.absolute.path}.'); + print( + 'If this was deliberate, pass the package to the analyze command with the --$_customAnalysisFlag flag and try again.'); + throw ToolExit(1); + } + + print('Activating tuneup package...'); + await processRunner.runAndStream( + 'pub', ['global', 'activate', 'tuneup'], + workingDir: packagesDir, exitOnError: true); + + await for (Directory package in getPackages()) { + if (isFlutterPackage(package, fileSystem)) { + await processRunner.runAndStream('flutter', ['packages', 'get'], + workingDir: package, exitOnError: true); + } else { + await processRunner.runAndStream('pub', ['get'], + workingDir: package, exitOnError: true); + } + } + + final List failingPackages = []; + await for (Directory package in getPlugins()) { + final int exitCode = await processRunner.runAndStream( + 'pub', ['global', 'run', 'tuneup', 'check'], + workingDir: package); + if (exitCode != 0) { + failingPackages.add(p.basename(package.path)); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print('The following packages have analyzer errors (see above):'); + failingPackages.forEach((String package) { + print(' * $package'); + }); + throw ToolExit(1); + } + + print('No analyzer errors found!'); + } +} diff --git a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart new file mode 100644 index 000000000000..1493b3cce05d --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart @@ -0,0 +1,191 @@ +// 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 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'common.dart'; + +class BuildExamplesCommand extends PluginCommand { + BuildExamplesCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag(kLinux, defaultsTo: false); + argParser.addFlag(kMacos, defaultsTo: false); + argParser.addFlag(kWindows, defaultsTo: false); + argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS); + argParser.addFlag(kApk); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Enables the given Dart SDK experiments.', + ); + } + + @override + final String name = 'build-examples'; + + @override + final String description = + 'Builds all example apps (IPA for iOS and APK for Android).\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + if (!argResults[kIpa] && + !argResults[kApk] && + !argResults[kLinux] && + !argResults[kMacos] && + !argResults[kWindows]) { + print( + 'None of --linux, --macos, --windows, --apk nor --ipa were specified, ' + 'so not building anything.'); + return; + } + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + + final String enableExperiment = argResults[kEnableExperiment]; + + checkSharding(); + final List failingPackages = []; + await for (Directory plugin in getPlugins()) { + for (Directory example in getExamplesForPlugin(plugin)) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + + if (argResults[kLinux]) { + print('\nBUILDING Linux for $packageName'); + if (isLinuxPlugin(plugin, fileSystem)) { + int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kLinux, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (linux)'); + } + } else { + print('Linux is not supported by this plugin'); + } + } + + if (argResults[kMacos]) { + print('\nBUILDING macOS for $packageName'); + if (isMacOsPlugin(plugin, fileSystem)) { + // TODO(https://github.com/flutter/flutter/issues/46236): + // Builing macos without running flutter pub get first results + // in an error. + int exitCode = await processRunner.runAndStream( + flutterCommand, ['pub', 'get'], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (macos)'); + } else { + exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kMacos, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (macos)'); + } + } + } else { + print('macOS is not supported by this plugin'); + } + } + + if (argResults[kWindows]) { + print('\nBUILDING Windows for $packageName'); + if (isWindowsPlugin(plugin, fileSystem)) { + int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kWindows, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (windows)'); + } + } else { + print('Windows is not supported by this plugin'); + } + } + + if (argResults[kIpa]) { + print('\nBUILDING IPA for $packageName'); + if (isIosPlugin(plugin, fileSystem)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + '--verbose', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example, + exitOnError: true); + if (exitCode != 0) { + failingPackages.add('$packageName (ipa)'); + } + } else { + print('iOS is not supported by this plugin'); + } + } + print('end ios build'); + + if (argResults[kApk]) { + print('\nBUILDING APK for $packageName'); + if (isAndroidPlugin(plugin, fileSystem)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + 'apk', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (apk)'); + } + } else { + print('Android is not supported by this plugin'); + } + } + } + } + print('\n\n'); + + if (failingPackages.isNotEmpty) { + print('The following build are failing (see above for details):'); + for (String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + + print('All builds successful!'); + } +} diff --git a/packages/flutter_plugin_tools/lib/src/common.dart b/packages/flutter_plugin_tools/lib/src/common.dart new file mode 100644 index 000000000000..78b91ee8a75b --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/common.dart @@ -0,0 +1,466 @@ +// 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 'dart:io' as io; +import 'dart:math'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; + +typedef void Print(Object object); + +/// Key for windows platform. +const String kWindows = 'windows'; + +/// Key for macos platform. +const String kMacos = 'macos'; + +/// Key for linux platform. +const String kLinux = 'linux'; + +/// Key for IPA (iOS) platform. +const String kIos = 'ios'; + +/// Key for APK (Android) platform. +const String kAndroid = 'android'; + +/// Key for Web platform. +const String kWeb = 'web'; + +/// Key for IPA. +const String kIpa = 'ipa'; + +/// Key for APK. +const String kApk = 'apk'; + +/// Key for enable experiment. +const String kEnableExperiment = 'enable-experiment'; + +/// Returns whether the given directory contains a Flutter package. +bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) { + if (entity == null || entity is! Directory) { + return false; + } + + try { + final File pubspecFile = + fileSystem.file(p.join(entity.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final YamlMap dependencies = pubspecYaml['dependencies']; + if (dependencies == null) { + return false; + } + return dependencies.containsKey('flutter'); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Returns whether the given directory contains a Flutter [platform] plugin. +/// +/// It checks this by looking for the following pattern in the pubspec: +/// +/// flutter: +/// plugin: +/// platforms: +/// [platform]: +bool pluginSupportsPlatform( + String platform, FileSystemEntity entity, FileSystem fileSystem) { + assert(platform == kIos || + platform == kAndroid || + platform == kWeb || + platform == kMacos || + platform == kWindows || + platform == kLinux); + if (entity == null || entity is! Directory) { + return false; + } + + try { + final File pubspecFile = + fileSystem.file(p.join(entity.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final YamlMap flutterSection = pubspecYaml['flutter']; + if (flutterSection == null) { + return false; + } + final YamlMap pluginSection = flutterSection['plugin']; + if (pluginSection == null) { + return false; + } + final YamlMap platforms = pluginSection['platforms']; + if (platforms == null) { + // Legacy plugin specs are assumed to support iOS and Android. + if (!pluginSection.containsKey('platforms')) { + return platform == kIos || platform == kAndroid; + } + return false; + } + return platforms.containsKey(platform); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Returns whether the given directory contains a Flutter Android plugin. +bool isAndroidPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kAndroid, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter iOS plugin. +bool isIosPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kIos, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter web plugin. +bool isWebPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kWeb, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter Windows plugin. +bool isWindowsPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kWindows, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter macOS plugin. +bool isMacOsPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kMacos, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter linux plugin. +bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kLinux, entity, fileSystem); +} + +/// Error thrown when a command needs to exit with a non-zero exit code. +class ToolExit extends Error { + ToolExit(this.exitCode); + + final int exitCode; +} + +abstract class PluginCommand extends Command { + PluginCommand( + this.packagesDir, + this.fileSystem, { + this.processRunner = const ProcessRunner(), + }) { + argParser.addMultiOption( + _pluginsArg, + splitCommas: true, + help: + 'Specifies which plugins the command should run on (before sharding).', + valueHelp: 'plugin1,plugin2,...', + ); + argParser.addOption( + _shardIndexArg, + help: 'Specifies the zero-based index of the shard to ' + 'which the command applies.', + valueHelp: 'i', + defaultsTo: '0', + ); + argParser.addOption( + _shardCountArg, + help: 'Specifies the number of shards into which plugins are divided.', + valueHelp: 'n', + defaultsTo: '1', + ); + argParser.addMultiOption( + _excludeArg, + abbr: 'e', + help: 'Exclude packages from this command.', + defaultsTo: [], + ); + } + + static const String _pluginsArg = 'plugins'; + static const String _shardIndexArg = 'shardIndex'; + static const String _shardCountArg = 'shardCount'; + static const String _excludeArg = 'exclude'; + + /// The directory containing the plugin packages. + final Directory packagesDir; + + /// The file system. + /// + /// This can be overridden for testing. + final FileSystem fileSystem; + + /// The process runner. + /// + /// This can be overridden for testing. + final ProcessRunner processRunner; + + int _shardIndex; + int _shardCount; + + int get shardIndex { + if (_shardIndex == null) { + checkSharding(); + } + return _shardIndex; + } + + int get shardCount { + if (_shardCount == null) { + checkSharding(); + } + return _shardCount; + } + + void checkSharding() { + final int shardIndex = int.tryParse(argResults[_shardIndexArg]); + final int shardCount = int.tryParse(argResults[_shardCountArg]); + if (shardIndex == null) { + usageException('$_shardIndexArg must be an integer'); + } + if (shardCount == null) { + usageException('$_shardCountArg must be an integer'); + } + if (shardCount < 1) { + usageException('$_shardCountArg must be positive'); + } + if (shardIndex < 0 || shardCount <= shardIndex) { + usageException( + '$_shardIndexArg must be in the half-open range [0..$shardCount['); + } + _shardIndex = shardIndex; + _shardCount = shardCount; + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution. + Stream getPlugins() async* { + // To avoid assuming consistency of `Directory.list` across command + // invocations, we collect and sort the plugin folders before sharding. + // This is considered an implementation detail which is why the API still + // uses streams. + final List allPlugins = await _getAllPlugins().toList(); + allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path)); + // Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2. + // Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3. + // Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0. + final int shardSize = allPlugins.length ~/ shardCount + + (allPlugins.length % shardCount == 0 ? 0 : 1); + final int start = min(shardIndex * shardSize, allPlugins.length); + final int end = min(start + shardSize, allPlugins.length); + + for (Directory plugin in allPlugins.sublist(start, end)) { + yield plugin; + } + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution, assuming there is only one shard. + /// + /// Plugin packages can exist in one of two places relative to the packages + /// directory. + /// + /// 1. As a Dart package in a directory which is a direct child of the + /// packages directory. This is a plugin where all of the implementations + /// exist in a single Dart package. + /// 2. Several plugin packages may live in a directory which is a direct + /// child of the packages directory. This directory groups several Dart + /// packages which implement a single plugin. This directory contains a + /// "client library" package, which declares the API for the plugin, as + /// well as one or more platform-specific implementations. + Stream _getAllPlugins() async* { + final Set plugins = Set.from(argResults[_pluginsArg]); + final Set excludedPlugins = + Set.from(argResults[_excludeArg]); + + await for (FileSystemEntity entity + in packagesDir.list(followLinks: false)) { + // A top-level Dart package is a plugin package. + if (_isDartPackage(entity)) { + if (!excludedPlugins.contains(entity.basename) && + (plugins.isEmpty || plugins.contains(p.basename(entity.path)))) { + yield entity; + } + } else if (entity is Directory) { + // Look for Dart packages under this top-level directory. + await for (FileSystemEntity subdir in entity.list(followLinks: false)) { + if (_isDartPackage(subdir)) { + // If --plugin=my_plugin is passed, then match all federated + // plugins under 'my_plugin'. Also match if the exact plugin is + // passed. + final String relativePath = + p.relative(subdir.path, from: packagesDir.path); + final String basenamePath = p.basename(entity.path); + if (!excludedPlugins.contains(basenamePath) && + !excludedPlugins.contains(relativePath) && + (plugins.isEmpty || + plugins.contains(relativePath) || + plugins.contains(basenamePath))) { + yield subdir; + } + } + } + } + } + } + + /// Returns the example Dart package folders of the plugins involved in this + /// command execution. + Stream getExamples() => + getPlugins().expand(getExamplesForPlugin); + + /// Returns all Dart package folders (typically, plugin + example) of the + /// plugins involved in this command execution. + Stream getPackages() async* { + await for (Directory plugin in getPlugins()) { + yield plugin; + yield* plugin + .list(recursive: true, followLinks: false) + .where(_isDartPackage) + .cast(); + } + } + + /// Returns the files contained, recursively, within the plugins + /// involved in this command execution. + Stream getFiles() { + return getPlugins().asyncExpand((Directory folder) => folder + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => entity is File) + .cast()); + } + + /// Returns whether the specified entity is a directory containing a + /// `pubspec.yaml` file. + bool _isDartPackage(FileSystemEntity entity) { + return entity is Directory && + fileSystem.file(p.join(entity.path, 'pubspec.yaml')).existsSync(); + } + + /// Returns the example Dart packages contained in the specified plugin, or + /// an empty List, if the plugin has no examples. + Iterable getExamplesForPlugin(Directory plugin) { + final Directory exampleFolder = + fileSystem.directory(p.join(plugin.path, 'example')); + if (!exampleFolder.existsSync()) { + return []; + } + if (isFlutterPackage(exampleFolder, fileSystem)) { + return [exampleFolder]; + } + // Only look at the subdirectories of the example directory if the example + // directory itself is not a Dart package, and only look one level below the + // example directory for other dart packages. + return exampleFolder + .listSync() + .where( + (FileSystemEntity entity) => isFlutterPackage(entity, fileSystem)) + .cast(); + } +} + +/// A class used to run processes. +/// +/// We use this instead of directly running the process so it can be overridden +/// in tests. +class ProcessRunner { + const ProcessRunner(); + + /// Run the [executable] with [args] and stream output to stderr and stdout. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// + /// Returns the exit code of the [executable]. + Future runAndStream( + String executable, + List args, { + Directory workingDir, + bool exitOnError = false, + }) async { + print( + 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDir?.path); + await io.stdout.addStream(process.stdout); + await io.stderr.addStream(process.stderr); + if (exitOnError && await process.exitCode != 0) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error See above for details.'); + throw ToolExit(await process.exitCode); + } + return process.exitCode; + } + + /// Run the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// + /// Returns the [io.ProcessResult] of the [executable]. + Future run(String executable, List args, + {Directory workingDir, + bool exitOnError = false, + stdoutEncoding = io.systemEncoding, + stderrEncoding = io.systemEncoding}) async { + return io.Process.run(executable, args, + workingDirectory: workingDir?.path, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding); + } + + /// Starts the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// Returns the started [io.Process]. + Future start(String executable, List args, + {Directory workingDirectory}) async { + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDirectory?.path); + return process; + } + + /// Run the [executable] with [args], throwing an error on non-zero exit code. + /// + /// Unlike [runAndStream], this does not stream the process output to stdout. + /// It also unconditionally throws an error on a non-zero exit code. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// Returns the [io.ProcessResult] of running the [executable]. + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + final io.ProcessResult result = await io.Process.run(executable, args, + workingDirectory: workingDir?.path); + if (result.exitCode != 0) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error Stderr:\n${result.stdout}'); + throw ToolExit(result.exitCode); + } + return result; + } + + String _getErrorString(String executable, List args, + {Directory workingDir}) { + final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; + return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; + } +} diff --git a/packages/flutter_plugin_tools/lib/src/create_all_plugins_app_command.dart b/packages/flutter_plugin_tools/lib/src/create_all_plugins_app_command.dart new file mode 100644 index 000000000000..0f1431c5aee0 --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/create_all_plugins_app_command.dart @@ -0,0 +1,200 @@ +// 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:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common.dart'; + +// TODO(cyanglaz): Add tests for this command. +// https://github.com/flutter/flutter/issues/61049 +class CreateAllPluginsAppCommand extends PluginCommand { + CreateAllPluginsAppCommand(Directory packagesDir, FileSystem fileSystem) + : super(packagesDir, fileSystem); + + @override + String get description => + 'Generate Flutter app that includes all plugins in packages.'; + + @override + String get name => 'all-plugins-app'; + + @override + Future run() async { + final int exitCode = await _createPlugin(); + if (exitCode != 0) { + throw ToolExit(exitCode); + } + + await Future.wait(>[ + _genPubspecWithAllPlugins(), + _updateAppGradle(), + _updateManifest(), + ]); + } + + Future _createPlugin() async { + final io.ProcessResult result = io.Process.runSync( + 'flutter', + [ + 'create', + '--template=app', + '--project-name=all_plugins', + '--android-language=java', + './all_plugins', + ], + ); + + print(result.stdout); + print(result.stderr); + return result.exitCode; + } + + Future _updateAppGradle() async { + final File gradleFile = fileSystem.file(p.join( + 'all_plugins', + 'android', + 'app', + 'build.gradle', + )); + if (!gradleFile.existsSync()) { + throw ToolExit(64); + } + + final StringBuffer newGradle = StringBuffer(); + for (String line in gradleFile.readAsLinesSync()) { + newGradle.writeln(line); + if (line.contains('defaultConfig {')) { + newGradle.writeln(' multiDexEnabled true'); + } else if (line.contains('dependencies {')) { + newGradle.writeln( + ' implementation \'com.google.guava:guava:27.0.1-android\'\n', + ); + // Tests for https://github.com/flutter/flutter/issues/43383 + newGradle.writeln( + " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", + ); + } + } + gradleFile.writeAsStringSync(newGradle.toString()); + } + + Future _updateManifest() async { + final File manifestFile = fileSystem.file(p.join( + 'all_plugins', + 'android', + 'app', + 'src', + 'main', + 'AndroidManifest.xml', + )); + if (!manifestFile.existsSync()) { + throw ToolExit(64); + } + + final StringBuffer newManifest = StringBuffer(); + for (String line in manifestFile.readAsLinesSync()) { + if (line.contains('package="com.example.all_plugins"')) { + newManifest + ..writeln('package="com.example.all_plugins"') + ..writeln('xmlns:tools="http://schemas.android.com/tools">') + ..writeln() + ..writeln( + '', + ); + } else { + newManifest.writeln(line); + } + } + manifestFile.writeAsStringSync(newManifest.toString()); + } + + Future _genPubspecWithAllPlugins() async { + final Map pluginDeps = + await _getValidPathDependencies(); + final Pubspec pubspec = Pubspec( + 'all_plugins', + description: 'Flutter app containing all 1st party plugins.', + version: Version.parse('1.0.0+1'), + environment: { + 'sdk': VersionConstraint.compatibleWith( + Version.parse('2.0.0'), + ), + }, + dependencies: { + 'flutter': SdkDependency('flutter'), + }..addAll(pluginDeps), + devDependencies: { + 'flutter_test': SdkDependency('flutter'), + }, + dependencyOverrides: pluginDeps, + ); + final File pubspecFile = + fileSystem.file(p.join('all_plugins', 'pubspec.yaml')); + pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); + } + + Future> _getValidPathDependencies() async { + final Map pathDependencies = + {}; + + await for (Directory package in getPlugins()) { + final String pluginName = package.path.split('/').last; + final File pubspecFile = + fileSystem.file(p.join(package.path, 'pubspec.yaml')); + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + + if (pubspec.publishTo != 'none') { + pathDependencies[pluginName] = PathDependency(package.path); + } + } + return pathDependencies; + } + + String _pubspecToString(Pubspec pubspec) { + return ''' +### Generated file. Do not edit. Run `pub global run flutter_plugin_tools gen-pubspec` to update. +name: ${pubspec.name} +description: ${pubspec.description} + +version: ${pubspec.version} + +environment:${_pubspecMapString(pubspec.environment)} + +dependencies:${_pubspecMapString(pubspec.dependencies)} + +dependency_overrides:${_pubspecMapString(pubspec.dependencyOverrides)} + +dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} +###'''; + } + + String _pubspecMapString(Map values) { + final StringBuffer buffer = StringBuffer(); + + for (MapEntry entry in values.entries) { + buffer.writeln(); + if (entry.value is VersionConstraint) { + buffer.write(' ${entry.key}: ${entry.value}'); + } else if (entry.value is SdkDependency) { + final SdkDependency dep = entry.value; + buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); + } else if (entry.value is PathDependency) { + final PathDependency dep = entry.value; + buffer.write(' ${entry.key}: \n path: ${dep.path}'); + } else { + throw UnimplementedError( + 'Not available for type: ${entry.value.runtimeType}', + ); + } + } + + return buffer.toString(); + } +} diff --git a/packages/flutter_plugin_tools/lib/src/drive_examples_command.dart b/packages/flutter_plugin_tools/lib/src/drive_examples_command.dart new file mode 100644 index 000000000000..8b1fa3624fbf --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/drive_examples_command.dart @@ -0,0 +1,210 @@ +// 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:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'common.dart'; + +class DriveExamplesCommand extends PluginCommand { + DriveExamplesCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag(kLinux, + help: 'Runs the Linux implementation of the examples'); + argParser.addFlag(kMacos, + help: 'Runs the macOS implementation of the examples'); + argParser.addFlag(kWindows, + help: 'Runs the Windows implementation of the examples'); + argParser.addFlag(kIos, + help: 'Runs the iOS implementation of the examples'); + argParser.addFlag(kAndroid, + help: 'Runs the Android implementation of the examples'); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: + 'Runs the driver tests in Dart VM with the given experiments enabled.', + ); + } + + @override + final String name = 'drive-examples'; + + @override + final String description = 'Runs driver tests for plugin example apps.\n\n' + 'For each *_test.dart in test_driver/ it drives an application with a ' + 'corresponding name in the test/ or test_driver/ directories.\n\n' + 'For example, test_driver/app_test.dart would match test/app.dart.\n\n' + 'This command requires "flutter" to be in your path.\n\n' + 'If a file with a corresponding name cannot be found, this driver file' + 'will be used to drive the tests that match ' + 'integration_test/*_test.dart.'; + + @override + Future run() async { + checkSharding(); + final List failingTests = []; + final bool isLinux = argResults[kLinux]; + final bool isMacos = argResults[kMacos]; + final bool isWindows = argResults[kWindows]; + await for (Directory plugin in getPlugins()) { + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + for (Directory example in getExamplesForPlugin(plugin)) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + if (!(await pluginSupportedOnCurrentPlatform(plugin, fileSystem))) { + continue; + } + final Directory driverTests = + fileSystem.directory(p.join(example.path, 'test_driver')); + if (!driverTests.existsSync()) { + // No driver tests available for this example + continue; + } + // Look for driver tests ending in _test.dart in test_driver/ + await for (FileSystemEntity test in driverTests.list()) { + final String driverTestName = + p.relative(test.path, from: driverTests.path); + if (!driverTestName.endsWith('_test.dart')) { + continue; + } + // Try to find a matching app to drive without the _test.dart + final String deviceTestName = driverTestName.replaceAll( + RegExp(r'_test.dart$'), + '.dart', + ); + String deviceTestPath = p.join('test', deviceTestName); + if (!fileSystem + .file(p.join(example.path, deviceTestPath)) + .existsSync()) { + // If the app isn't in test/ folder, look in test_driver/ instead. + deviceTestPath = p.join('test_driver', deviceTestName); + } + + final List targetPaths = []; + if (fileSystem + .file(p.join(example.path, deviceTestPath)) + .existsSync()) { + targetPaths.add(deviceTestPath); + } else { + final Directory integrationTests = + fileSystem.directory(p.join(example.path, 'integration_test')); + + if (await integrationTests.exists()) { + await for (FileSystemEntity integration_test + in integrationTests.list()) { + if (!integration_test.basename.endsWith('_test.dart')) { + continue; + } + targetPaths + .add(p.relative(integration_test.path, from: example.path)); + } + } + + if (targetPaths.isEmpty) { + print(''' +Unable to infer a target application for $driverTestName to drive. +Tried searching for the following: +1. test/$deviceTestName +2. test_driver/$deviceTestName +3. test_driver/*_test.dart +'''); + failingTests.add(p.relative(test.path, from: example.path)); + continue; + } + } + + final List driveArgs = ['drive', '-v']; + + final String enableExperiment = argResults[kEnableExperiment]; + if (enableExperiment.isNotEmpty) { + driveArgs.add('--enable-experiment=$enableExperiment'); + } + + if (isLinux && isLinuxPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'linux', + ]); + } + if (isMacos && isMacOsPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'macos', + ]); + } + if (isWindows && isWindowsPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'windows', + ]); + } + + for (final targetPath in targetPaths) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + ...driveArgs, + '--driver', + p.join('test_driver', driverTestName), + '--target', + targetPath, + ], + workingDir: example, + exitOnError: true); + if (exitCode != 0) { + failingTests.add(p.join(packageName, deviceTestPath)); + } + } + } + } + } + print('\n\n'); + + if (failingTests.isNotEmpty) { + print('The following driver tests are failing (see above for details):'); + for (String test in failingTests) { + print(' * $test'); + } + throw ToolExit(1); + } + + print('All driver tests successful!'); + } + + Future pluginSupportedOnCurrentPlatform( + FileSystemEntity plugin, FileSystem fileSystem) async { + final bool isLinux = argResults[kLinux]; + final bool isMacos = argResults[kMacos]; + final bool isWindows = argResults[kWindows]; + final bool isIOS = argResults[kIos]; + final bool isAndroid = argResults[kAndroid]; + if (isLinux) { + return isLinuxPlugin(plugin, fileSystem); + } + if (isMacos) { + return isMacOsPlugin(plugin, fileSystem); + } + if (isWindows) { + return isWindowsPlugin(plugin, fileSystem); + } + if (isIOS) { + return isIosPlugin(plugin, fileSystem); + } + if (isAndroid) { + return (isAndroidPlugin(plugin, fileSystem)); + } + // When we are here, no flags are specified. Only return true if the plugin supports mobile for legacy command support. + // TODO(cyanglaz): Make mobile platforms flags also required like other platforms (breaking change). + // https://github.com/flutter/flutter/issues/58285 + final bool isMobilePlugin = + isIosPlugin(plugin, fileSystem) || isAndroidPlugin(plugin, fileSystem); + return isMobilePlugin; + } +} diff --git a/packages/flutter_plugin_tools/lib/src/firebase_test_lab_command.dart b/packages/flutter_plugin_tools/lib/src/firebase_test_lab_command.dart new file mode 100644 index 000000000000..0b4b2a471dbc --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/firebase_test_lab_command.dart @@ -0,0 +1,264 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:uuid/uuid.dart'; + +import 'common.dart'; + +class FirebaseTestLabCommand extends PluginCommand { + FirebaseTestLabCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + Print print = print, + }) : _print = print, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + 'project', + defaultsTo: 'flutter-infra', + help: 'The Firebase project name.', + ); + argParser.addOption('service-key', + defaultsTo: + p.join(io.Platform.environment['HOME'], 'gcloud-service-key.json')); + argParser.addOption('test-run-id', + defaultsTo: Uuid().v4(), + help: + 'Optional string to append to the results path, to avoid conflicts. ' + 'Randomly chosen on each invocation if none is provided. ' + 'The default shown here is just an example.'); + argParser.addMultiOption('device', + splitCommas: false, + defaultsTo: [ + 'model=walleye,version=26', + 'model=flame,version=29' + ], + help: + 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); + argParser.addOption('results-bucket', + defaultsTo: 'gs://flutter_firebase_testlab'); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Enables the given Dart SDK experiments.', + ); + } + + @override + final String name = 'firebase-test-lab'; + + @override + final String description = 'Runs the instrumentation tests of the example ' + 'apps on Firebase Test Lab.\n\n' + 'Runs tests in test_instrumentation folder using the ' + 'instrumentation_test package.'; + + static const String _gradleWrapper = 'gradlew'; + + final Print _print; + + Completer _firebaseProjectConfigured; + + Future _configureFirebaseProject() async { + if (_firebaseProjectConfigured != null) { + return _firebaseProjectConfigured.future; + } else { + _firebaseProjectConfigured = Completer(); + } + await processRunner.runAndExitOnError('gcloud', [ + 'auth', + 'activate-service-account', + '--key-file=${argResults['service-key']}', + ]); + int exitCode = await processRunner.runAndStream('gcloud', [ + 'config', + 'set', + 'project', + argResults['project'], + ]); + if (exitCode == 0) { + _print('\nFirebase project configured.'); + return; + } else { + _print( + '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.'); + } + _firebaseProjectConfigured.complete(null); + } + + @override + Future run() async { + checkSharding(); + final Stream packagesWithTests = getPackages().where( + (Directory d) => + isFlutterPackage(d, fileSystem) && + fileSystem + .directory(p.join( + d.path, 'example', 'android', 'app', 'src', 'androidTest')) + .existsSync()); + + final List failingPackages = []; + final List missingFlutterBuild = []; + int resultsCounter = + 0; // We use a unique GCS bucket for each Firebase Test Lab run + await for (Directory package in packagesWithTests) { + // See https://github.com/flutter/flutter/issues/38983 + + final Directory exampleDirectory = + fileSystem.directory(p.join(package.path, 'example')); + final String packageName = + p.relative(package.path, from: packagesDir.path); + _print('\nRUNNING FIREBASE TEST LAB TESTS for $packageName'); + + final Directory androidDirectory = + fileSystem.directory(p.join(exampleDirectory.path, 'android')); + + final String enableExperiment = argResults[kEnableExperiment]; + final String encodedEnableExperiment = + Uri.encodeComponent('--enable-experiment=$enableExperiment'); + + // Ensures that gradle wrapper exists + if (!fileSystem + .file(p.join(androidDirectory.path, _gradleWrapper)) + .existsSync()) { + final int exitCode = await processRunner.runAndStream( + 'flutter', + [ + 'build', + 'apk', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + continue; + } + + await _configureFirebaseProject(); + + int exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + [ + 'app:assembleAndroidTest', + '-Pverbose=true', + if (enableExperiment.isNotEmpty) + '-Pextra-front-end-options=$encodedEnableExperiment', + if (enableExperiment.isNotEmpty) + '-Pextra-gen-snapshot-options=$encodedEnableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + + // Look for tests recursively in folders that start with 'test' and that + // live in the root or example folders. + bool isTestDir(FileSystemEntity dir) { + return p.basename(dir.path).startsWith('test') || + p.basename(dir.path) == 'integration_test'; + } + + final List testDirs = + package.listSync().where(isTestDir).toList(); + final Directory example = + fileSystem.directory(p.join(package.path, 'example')); + testDirs.addAll(example.listSync().where(isTestDir).toList()); + for (Directory testDir in testDirs) { + bool isE2ETest(FileSystemEntity file) { + return file.path.endsWith('_e2e.dart') || + (file.parent.basename == 'integration_test' && + file.path.endsWith('_test.dart')); + } + + final List testFiles = testDir + .listSync(recursive: true, followLinks: true) + .where(isE2ETest) + .toList(); + for (FileSystemEntity test in testFiles) { + exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + [ + 'app:assembleDebug', + '-Pverbose=true', + '-Ptarget=${test.path}', + if (enableExperiment.isNotEmpty) + '-Pextra-front-end-options=$encodedEnableExperiment', + if (enableExperiment.isNotEmpty) + '-Pextra-gen-snapshot-options=$encodedEnableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + final String buildId = io.Platform.environment['CIRRUS_BUILD_ID']; + final String testRunId = argResults['test-run-id']; + final String resultsDir = + 'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/'; + final List args = [ + 'firebase', + 'test', + 'android', + 'run', + '--type', + 'instrumentation', + '--app', + 'build/app/outputs/apk/debug/app-debug.apk', + '--test', + 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', + '--timeout', + '5m', + '--results-bucket=${argResults['results-bucket']}', + '--results-dir=${resultsDir}', + ]; + for (String device in argResults['device']) { + args.addAll(['--device', device]); + } + exitCode = await processRunner.runAndStream('gcloud', args, + workingDir: exampleDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + } + } + } + + _print('\n\n'); + if (failingPackages.isNotEmpty) { + _print( + 'The instrumentation tests for the following packages are failing (see above for' + 'details):'); + for (String package in failingPackages) { + _print(' * $package'); + } + } + if (missingFlutterBuild.isNotEmpty) { + _print('Run "pub global run flutter_plugin_tools build-examples --apk" on' + 'the following packages before executing tests again:'); + for (String package in missingFlutterBuild) { + _print(' * $package'); + } + } + + if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { + throw ToolExit(1); + } + + _print('All Firebase Test Lab tests successful!'); + } +} diff --git a/packages/flutter_plugin_tools/lib/src/format_command.dart b/packages/flutter_plugin_tools/lib/src/format_command.dart new file mode 100644 index 000000000000..ec326b96c1f9 --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/format_command.dart @@ -0,0 +1,147 @@ +// 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 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as p; +import 'package:quiver/iterables.dart'; + +import 'common.dart'; + +const String _googleFormatterUrl = + 'https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'; + +class FormatCommand extends PluginCommand { + FormatCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag('travis', hide: true); + argParser.addOption('clang-format', + defaultsTo: 'clang-format', + help: 'Path to executable of clang-format v5.'); + } + + @override + final String name = 'format'; + + @override + final String description = + 'Formats the code of all packages (Java, Objective-C, C++, and Dart).\n\n' + 'This command requires "git", "flutter" and "clang-format" v5 to be in ' + 'your path.'; + + @override + Future run() async { + checkSharding(); + final String googleFormatterPath = await _getGoogleFormatterPath(); + + await _formatDart(); + await _formatJava(googleFormatterPath); + await _formatCppAndObjectiveC(); + + if (argResults['travis']) { + final bool modified = await _didModifyAnything(); + if (modified) { + throw ToolExit(1); + } + } + } + + Future _didModifyAnything() async { + final io.ProcessResult modifiedFiles = await processRunner + .runAndExitOnError('git', ['ls-files', '--modified'], + workingDir: packagesDir); + + print('\n\n'); + + if (modifiedFiles.stdout.isEmpty) { + print('All files formatted correctly.'); + return false; + } + + print('These files are not formatted correctly (see diff below):'); + LineSplitter.split(modifiedFiles.stdout) + .map((String line) => ' $line') + .forEach(print); + + print('\nTo fix run "pub global activate flutter_plugin_tools && ' + 'pub global run flutter_plugin_tools format" or copy-paste ' + 'this command into your terminal:'); + + print('patch -p1 <['diff'], workingDir: packagesDir); + print(diff.stdout); + print('DONE'); + return true; + } + + Future _formatCppAndObjectiveC() async { + print('Formatting all .cc, .cpp, .mm, .m, and .h files...'); + final Iterable allFiles = [] + ..addAll(await _getFilesWithExtension('.h')) + ..addAll(await _getFilesWithExtension('.m')) + ..addAll(await _getFilesWithExtension('.mm')) + ..addAll(await _getFilesWithExtension('.cc')) + ..addAll(await _getFilesWithExtension('.cpp')); + // Split this into multiple invocations to avoid a + // 'ProcessException: Argument list too long'. + final Iterable> batches = partition(allFiles, 100); + for (List batch in batches) { + await processRunner.runAndStream(argResults['clang-format'], + ['-i', '--style=Google']..addAll(batch), + workingDir: packagesDir, exitOnError: true); + } + } + + Future _formatJava(String googleFormatterPath) async { + print('Formatting all .java files...'); + final Iterable javaFiles = await _getFilesWithExtension('.java'); + await processRunner.runAndStream('java', + ['-jar', googleFormatterPath, '--replace']..addAll(javaFiles), + workingDir: packagesDir, exitOnError: true); + } + + Future _formatDart() async { + // This actually should be fine for non-Flutter Dart projects, no need to + // specifically shell out to dartfmt -w in that case. + print('Formatting all .dart files...'); + final Iterable dartFiles = await _getFilesWithExtension('.dart'); + if (dartFiles.isEmpty) { + print( + 'No .dart files to format. If you set the `--exclude` flag, most likey they were skipped'); + } else { + await processRunner.runAndStream( + 'flutter', ['format']..addAll(dartFiles), + workingDir: packagesDir, exitOnError: true); + } + } + + Future> _getFilesWithExtension(String extension) async => + getFiles() + .where((File file) => p.extension(file.path) == extension) + .map((File file) => file.path) + .toList(); + + Future _getGoogleFormatterPath() async { + final String javaFormatterPath = p.join( + p.dirname(p.fromUri(io.Platform.script)), + 'google-java-format-1.3-all-deps.jar'); + final File javaFormatterFile = fileSystem.file(javaFormatterPath); + + if (!javaFormatterFile.existsSync()) { + print('Downloading Google Java Format...'); + final http.Response response = await http.get(_googleFormatterUrl); + javaFormatterFile.writeAsBytesSync(response.bodyBytes); + } + + return javaFormatterPath; + } +} diff --git a/packages/flutter_plugin_tools/lib/src/java_test_command.dart b/packages/flutter_plugin_tools/lib/src/java_test_command.dart new file mode 100644 index 000000000000..cf605bfc5ce2 --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/java_test_command.dart @@ -0,0 +1,89 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class JavaTestCommand extends PluginCommand { + JavaTestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner); + + @override + final String name = 'java-test'; + + @override + final String description = 'Runs the Java tests of the example apps.\n\n' + 'Building the apks of the example apps is required before executing this' + 'command.'; + + static const String _gradleWrapper = 'gradlew'; + + @override + Future run() async { + checkSharding(); + final Stream examplesWithTests = getExamples().where( + (Directory d) => + isFlutterPackage(d, fileSystem) && + fileSystem + .directory(p.join(d.path, 'android', 'app', 'src', 'test')) + .existsSync()); + + final List failingPackages = []; + final List missingFlutterBuild = []; + await for (Directory example in examplesWithTests) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + print('\nRUNNING JAVA TESTS for $packageName'); + + final Directory androidDirectory = + fileSystem.directory(p.join(example.path, 'android')); + if (!fileSystem + .file(p.join(androidDirectory.path, _gradleWrapper)) + .existsSync()) { + print('ERROR: Run "flutter build apk" on example app of $packageName' + 'before executing tests.'); + missingFlutterBuild.add(packageName); + continue; + } + + final int exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + ['testDebugUnitTest', '--info'], + workingDir: androidDirectory); + if (exitCode != 0) { + failingPackages.add(packageName); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print( + 'The Java tests for the following packages are failing (see above for' + 'details):'); + for (String package in failingPackages) { + print(' * $package'); + } + } + if (missingFlutterBuild.isNotEmpty) { + print('Run "pub global run flutter_plugin_tools build-examples --apk" on' + 'the following packages before executing tests again:'); + for (String package in missingFlutterBuild) { + print(' * $package'); + } + } + + if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { + throw ToolExit(1); + } + + print('All Java tests successful!'); + } +} diff --git a/packages/flutter_plugin_tools/lib/src/lint_podspecs_command.dart b/packages/flutter_plugin_tools/lib/src/lint_podspecs_command.dart new file mode 100644 index 000000000000..68fd4b61dd66 --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/lint_podspecs_command.dart @@ -0,0 +1,146 @@ +// 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 'dart:convert'; +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'common.dart'; + +typedef void Print(Object object); + +/// Lint the CocoaPod podspecs, run the static analyzer on iOS/macOS plugin +/// platform code, and run unit tests. +/// +/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +class LintPodspecsCommand extends PluginCommand { + LintPodspecsCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), + Print print = print, + }) : _print = print, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addMultiOption('skip', + help: + 'Skip all linting for podspecs with this basename (example: federated plugins with placeholder podspecs)', + valueHelp: 'podspec_file_name'); + argParser.addMultiOption('ignore-warnings', + help: + 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs with this basename (example: plugins with known warnings)', + valueHelp: 'podspec_file_name'); + argParser.addMultiOption('no-analyze', + help: + 'Do not pass --analyze flag to "pod lib lint" for podspecs with this basename (example: plugins with known analyzer warnings)', + valueHelp: 'podspec_file_name'); + } + + @override + final String name = 'podspecs'; + + @override + List get aliases => ['podspec']; + + @override + final String description = + 'Runs "pod lib lint" on all iOS and macOS plugin podspecs.\n\n' + 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; + + final Platform platform; + + final Print _print; + + @override + Future run() async { + if (!platform.isMacOS) { + _print('Detected platform is not macOS, skipping podspec lint'); + return; + } + + checkSharding(); + + await processRunner.runAndExitOnError('which', ['pod'], + workingDir: packagesDir); + + _print('Starting podspec lint test'); + + final List failingPlugins = []; + for (File podspec in await _podspecsToLint()) { + if (!await _lintPodspec(podspec)) { + failingPlugins.add(p.basenameWithoutExtension(podspec.path)); + } + } + + _print('\n\n'); + if (failingPlugins.isNotEmpty) { + _print('The following plugins have podspec errors (see above):'); + failingPlugins.forEach((String plugin) { + _print(' * $plugin'); + }); + throw ToolExit(1); + } + } + + Future> _podspecsToLint() async { + final List podspecs = await getFiles().where((File entity) { + final String filePath = entity.path; + return p.extension(filePath) == '.podspec' && + !argResults['skip'].contains(p.basenameWithoutExtension(filePath)); + }).toList(); + + podspecs.sort( + (File a, File b) => p.basename(a.path).compareTo(p.basename(b.path))); + return podspecs; + } + + Future _lintPodspec(File podspec) async { + // Do not run the static analyzer on plugins with known analyzer issues. + final String podspecPath = podspec.path; + final bool runAnalyzer = !argResults['no-analyze'] + .contains(p.basenameWithoutExtension(podspecPath)); + + final String podspecBasename = p.basename(podspecPath); + if (runAnalyzer) { + _print('Linting and analyzing $podspecBasename'); + } else { + _print('Linting $podspecBasename'); + } + + // Lint plugin as framework (use_frameworks!). + final ProcessResult frameworkResult = await _runPodLint(podspecPath, + runAnalyzer: runAnalyzer, libraryLint: true); + _print(frameworkResult.stdout); + _print(frameworkResult.stderr); + + // Lint plugin as library. + final ProcessResult libraryResult = await _runPodLint(podspecPath, + runAnalyzer: runAnalyzer, libraryLint: false); + _print(libraryResult.stdout); + _print(libraryResult.stderr); + + return frameworkResult.exitCode == 0 && libraryResult.exitCode == 0; + } + + Future _runPodLint(String podspecPath, + {bool runAnalyzer, bool libraryLint}) async { + final bool allowWarnings = argResults['ignore-warnings'] + .contains(p.basenameWithoutExtension(podspecPath)); + final List arguments = [ + 'lib', + 'lint', + podspecPath, + if (allowWarnings) '--allow-warnings', + if (runAnalyzer) '--analyze', + if (libraryLint) '--use-libraries' + ]; + + return processRunner.run('pod', arguments, + workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); + } +} diff --git a/packages/flutter_plugin_tools/lib/src/list_command.dart b/packages/flutter_plugin_tools/lib/src/list_command.dart new file mode 100644 index 000000000000..7f94daac7096 --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/list_command.dart @@ -0,0 +1,60 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:file/file.dart'; + +import 'common.dart'; + +class ListCommand extends PluginCommand { + ListCommand(Directory packagesDir, FileSystem fileSystem) + : super(packagesDir, fileSystem) { + argParser.addOption( + _type, + defaultsTo: _plugin, + allowed: [_plugin, _example, _package, _file], + help: 'What type of file system content to list.', + ); + } + + static const String _type = 'type'; + static const String _plugin = 'plugin'; + static const String _example = 'example'; + static const String _package = 'package'; + static const String _file = 'file'; + + @override + final String name = 'list'; + + @override + final String description = 'Lists packages or files'; + + @override + Future run() async { + checkSharding(); + switch (argResults[_type]) { + case _plugin: + await for (Directory package in getPlugins()) { + print(package.path); + } + break; + case _example: + await for (Directory package in getExamples()) { + print(package.path); + } + break; + case _package: + await for (Directory package in getPackages()) { + print(package.path); + } + break; + case _file: + await for (File file in getFiles()) { + print(file.path); + } + break; + } + } +} diff --git a/packages/flutter_plugin_tools/lib/src/main.dart b/packages/flutter_plugin_tools/lib/src/main.dart new file mode 100644 index 000000000000..bb3f67c0a9e1 --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/main.dart @@ -0,0 +1,63 @@ +// 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:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; +import 'package:path/path.dart' as p; + +import 'analyze_command.dart'; +import 'build_examples_command.dart'; +import 'common.dart'; +import 'create_all_plugins_app_command.dart'; +import 'drive_examples_command.dart'; +import 'firebase_test_lab_command.dart'; +import 'format_command.dart'; +import 'java_test_command.dart'; +import 'lint_podspecs_command.dart'; +import 'list_command.dart'; +import 'test_command.dart'; +import 'version_check_command.dart'; +import 'xctest_command.dart'; + +void main(List args) { + final FileSystem fileSystem = const LocalFileSystem(); + + Directory packagesDir = fileSystem + .directory(p.join(fileSystem.currentDirectory.path, 'packages')); + + if (!packagesDir.existsSync()) { + if (p.basename(fileSystem.currentDirectory.path) == 'packages') { + packagesDir = fileSystem.currentDirectory; + } else { + print('Error: Cannot find a "packages" sub-directory'); + io.exit(1); + } + } + + final CommandRunner commandRunner = CommandRunner( + 'pub global run flutter_plugin_tools', + 'Productivity utils for hosting multiple plugins within one repository.') + ..addCommand(AnalyzeCommand(packagesDir, fileSystem)) + ..addCommand(BuildExamplesCommand(packagesDir, fileSystem)) + ..addCommand(CreateAllPluginsAppCommand(packagesDir, fileSystem)) + ..addCommand(DriveExamplesCommand(packagesDir, fileSystem)) + ..addCommand(FirebaseTestLabCommand(packagesDir, fileSystem)) + ..addCommand(FormatCommand(packagesDir, fileSystem)) + ..addCommand(JavaTestCommand(packagesDir, fileSystem)) + ..addCommand(LintPodspecsCommand(packagesDir, fileSystem)) + ..addCommand(ListCommand(packagesDir, fileSystem)) + ..addCommand(PublishPluginCommand(packagesDir, fileSystem)) + ..addCommand(TestCommand(packagesDir, fileSystem)) + ..addCommand(VersionCheckCommand(packagesDir, fileSystem)) + ..addCommand(XCTestCommand(packagesDir, fileSystem)); + + commandRunner.run(args).catchError((Object e) { + final ToolExit toolExit = e; + io.exit(toolExit.exitCode); + }, test: (Object e) => e is ToolExit); +} diff --git a/packages/flutter_plugin_tools/lib/src/publish_plugin_command.dart b/packages/flutter_plugin_tools/lib/src/publish_plugin_command.dart new file mode 100644 index 000000000000..55c48f5484e5 --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/publish_plugin_command.dart @@ -0,0 +1,227 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; + +import 'common.dart'; + +/// Wraps pub publish with a few niceties used by the flutter/plugin team. +/// +/// 1. Checks for any modified files in git and refuses to publish if there's an +/// issue. +/// 2. Tags the release with the format -v. +/// 3. Pushes the release to a remote. +/// +/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full +/// usage information. +/// +/// [processRunner], [print], and [stdin] can be overriden for easier testing. +class PublishPluginCommand extends PluginCommand { + PublishPluginCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + Print print = print, + Stdin stdinput, + }) : _print = print, + _stdin = stdinput ?? stdin, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + _packageOption, + help: 'The package to publish.' + 'If the package directory name is different than its pubspec.yaml name, then this should specify the directory.', + ); + argParser.addMultiOption(_pubFlagsOption, + help: + 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); + argParser.addFlag( + _tagReleaseOption, + help: 'Whether or not to tag the release.', + defaultsTo: true, + negatable: true, + ); + argParser.addFlag( + _pushTagsOption, + help: + 'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.', + defaultsTo: true, + negatable: true, + ); + argParser.addOption( + _remoteOption, + help: + 'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.', + // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. + defaultsTo: 'upstream', + ); + } + + static const String _packageOption = 'package'; + static const String _tagReleaseOption = 'tag-release'; + static const String _pushTagsOption = 'push-tags'; + static const String _pubFlagsOption = 'pub-publish-flags'; + static const String _remoteOption = 'remote'; + + // Version tags should follow -v. For example, + // `flutter_plugin_tools-v0.0.24`. + static const String _tagFormat = '%PACKAGE%-v%VERSION%'; + + @override + final String name = 'publish-plugin'; + + @override + final String description = + 'Attempts to publish the given plugin and tag its release on GitHub.'; + + final Print _print; + final Stdin _stdin; + // The directory of the actual package that we are publishing. + Directory _packageDir; + StreamSubscription _stdinSubscription; + + @override + Future run() async { + checkSharding(); + _print('Checking local repo...'); + _packageDir = _checkPackageDir(); + await _checkGitStatus(); + final bool shouldPushTag = argResults[_pushTagsOption]; + final String remote = argResults[_remoteOption]; + String remoteUrl; + if (shouldPushTag) { + remoteUrl = await _verifyRemote(remote); + } + _print('Local repo is ready!'); + + await _publish(); + _print('Package published!'); + if (!argResults[_tagReleaseOption]) { + return await _finishSuccesfully(); + } + + _print('Tagging release...'); + final String tag = _getTag(); + await processRunner.runAndExitOnError('git', ['tag', tag], + workingDir: _packageDir); + if (!shouldPushTag) { + return await _finishSuccesfully(); + } + + _print('Pushing tag to $remote...'); + await _pushTagToRemote(remote: remote, tag: tag, remoteUrl: remoteUrl); + await _finishSuccesfully(); + } + + Future _finishSuccesfully() async { + await _stdinSubscription.cancel(); + _print('Done!'); + } + + Directory _checkPackageDir() { + final String package = argResults[_packageOption]; + if (package == null) { + _print( + 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); + throw ToolExit(1); + } + final Directory _packageDir = packagesDir.childDirectory(package); + if (!_packageDir.existsSync()) { + _print('${_packageDir.absolute.path} does not exist.'); + throw ToolExit(1); + } + if (!isFlutterPackage(_packageDir, fileSystem)) { + _print('${_packageDir.absolute.path} is not a flutter package.'); + throw ToolExit(1); + } + return _packageDir; + } + + Future _checkGitStatus() async { + if (!await GitDir.isGitDir(packagesDir.path)) { + _print('$packagesDir is not a valid Git repository.'); + throw ToolExit(1); + } + + final ProcessResult statusResult = await processRunner.runAndExitOnError( + 'git', + [ + 'status', + '--porcelain', + '--ignored', + _packageDir.absolute.path + ], + workingDir: _packageDir); + final String statusOutput = statusResult.stdout; + if (statusOutput.isNotEmpty) { + _print( + "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" + '$statusOutput\n' + 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); + throw ToolExit(1); + } + } + + Future _verifyRemote(String remote) async { + final ProcessResult remoteInfo = await processRunner.runAndExitOnError( + 'git', ['remote', 'get-url', remote], + workingDir: _packageDir); + return remoteInfo.stdout; + } + + Future _publish() async { + final List publishFlags = argResults[_pubFlagsOption]; + _print( + 'Running `pub publish ${publishFlags.join(' ')}` in ${_packageDir.absolute.path}...\n'); + final Process publish = await processRunner.start( + 'flutter', ['pub', 'publish'] + publishFlags, + workingDirectory: _packageDir); + publish.stdout + .transform(utf8.decoder) + .listen((String data) => _print(data)); + publish.stderr + .transform(utf8.decoder) + .listen((String data) => _print(data)); + _stdinSubscription = _stdin + .transform(utf8.decoder) + .listen((String data) => publish.stdin.writeln(data)); + final int result = await publish.exitCode; + if (result != 0) { + _print('Publish failed. Exiting.'); + throw ToolExit(result); + } + } + + String _getTag() { + final File pubspecFile = + fileSystem.file(p.join(_packageDir.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final String name = pubspecYaml['name']; + final String version = pubspecYaml['version']; + // We should have failed to publish if these were unset. + assert(name.isNotEmpty && version.isNotEmpty); + return _tagFormat + .replaceAll('%PACKAGE%', name) + .replaceAll('%VERSION%', version); + } + + Future _pushTagToRemote( + {@required String remote, + @required String tag, + @required String remoteUrl}) async { + assert(remote != null && tag != null && remoteUrl != null); + _print('Ready to push $tag to $remoteUrl (y/n)?'); + final String input = _stdin.readLineSync(); + if (input.toLowerCase() != 'y') { + _print('Tag push canceled.'); + throw ToolExit(1); + } + + await processRunner.runAndExitOnError('git', ['push', remote, tag], + workingDir: packagesDir); + } +} diff --git a/packages/flutter_plugin_tools/lib/src/test_command.dart b/packages/flutter_plugin_tools/lib/src/test_command.dart new file mode 100644 index 000000000000..e938168cfa89 --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/test_command.dart @@ -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. + +import 'dart:async'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class TestCommand extends PluginCommand { + TestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Runs the tests in Dart VM with the given experiments enabled.', + ); + } + + @override + final String name = 'test'; + + @override + final String description = 'Runs the Dart tests for all packages.\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + final List failingPackages = []; + await for (Directory packageDir in getPackages()) { + final String packageName = + p.relative(packageDir.path, from: packagesDir.path); + if (!fileSystem.directory(p.join(packageDir.path, 'test')).existsSync()) { + print('SKIPPING $packageName - no test subdirectory'); + continue; + } + + print('RUNNING $packageName tests...'); + + final String enableExperiment = argResults[kEnableExperiment]; + + // `flutter test` automatically gets packages. `pub run test` does not. :( + int exitCode = 0; + if (isFlutterPackage(packageDir, fileSystem)) { + final List args = [ + 'test', + '--color', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ]; + + if (isWebPlugin(packageDir, fileSystem)) { + args.add('--platform=chrome'); + } + exitCode = await processRunner.runAndStream( + 'flutter', + args, + workingDir: packageDir, + ); + } else { + exitCode = await processRunner.runAndStream( + 'pub', + ['get'], + workingDir: packageDir, + ); + if (exitCode == 0) { + exitCode = await processRunner.runAndStream( + 'pub', + [ + 'run', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + 'test', + ], + workingDir: packageDir, + ); + } + } + if (exitCode != 0) { + failingPackages.add(packageName); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print('Tests for the following packages are failing (see above):'); + failingPackages.forEach((String package) { + print(' * $package'); + }); + throw ToolExit(1); + } + + print('All tests are passing!'); + } +} diff --git a/packages/flutter_plugin_tools/lib/src/version_check_command.dart b/packages/flutter_plugin_tools/lib/src/version_check_command.dart new file mode 100644 index 000000000000..2c6b92bbcb7a --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/version_check_command.dart @@ -0,0 +1,220 @@ +// 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 'dart:io' as io; + +import 'package:meta/meta.dart'; +import 'package:colorize/colorize.dart'; +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:yaml/yaml.dart'; + +import 'common.dart'; + +const String _kBaseSha = 'base_sha'; + +class GitVersionFinder { + GitVersionFinder(this.baseGitDir, this.baseSha); + + final GitDir baseGitDir; + final String baseSha; + + static bool isPubspec(String file) { + return file.trim().endsWith('pubspec.yaml'); + } + + Future> getChangedPubSpecs() async { + final io.ProcessResult changedFilesCommand = await baseGitDir + .runCommand(['diff', '--name-only', '$baseSha', 'HEAD']); + final List changedFiles = + changedFilesCommand.stdout.toString().split('\n'); + return changedFiles.where(isPubspec).toList(); + } + + Future getPackageVersion(String pubspecPath, String gitRef) async { + final io.ProcessResult gitShow = + await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); + final String fileContent = gitShow.stdout; + final String versionString = loadYaml(fileContent)['version']; + return versionString == null ? null : Version.parse(versionString); + } +} + +enum NextVersionType { + BREAKING_MAJOR, + MAJOR_NULLSAFETY_PRE_RELEASE, + MINOR_NULLSAFETY_PRE_RELEASE, + MINOR, + PATCH, + RELEASE, +} + +Version getNextNullSafetyPreRelease(Version current, Version next) { + String nextNullsafetyPrerelease = 'nullsafety'; + if (current.isPreRelease && + current.preRelease.first is String && + current.preRelease.first == 'nullsafety') { + if (current.preRelease.length == 1) { + nextNullsafetyPrerelease = 'nullsafety.1'; + } else if (current.preRelease.length == 2 && + current.preRelease.last is int) { + nextNullsafetyPrerelease = 'nullsafety.${current.preRelease.last + 1}'; + } + } + return Version( + next.major, + next.minor, + next.patch, + pre: nextNullsafetyPrerelease, + ); +} + +@visibleForTesting +Map getAllowedNextVersions( + Version masterVersion, Version headVersion) { + final Version nextNullSafetyMajor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMajor); + final Version nextNullSafetyMinor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); + final Map allowedNextVersions = + { + masterVersion.nextMajor: NextVersionType.BREAKING_MAJOR, + nextNullSafetyMajor: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE, + nextNullSafetyMinor: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE, + masterVersion.nextMinor: NextVersionType.MINOR, + masterVersion.nextPatch: NextVersionType.PATCH, + }; + + if (masterVersion.major < 1 && headVersion.major < 1) { + int nextBuildNumber = -1; + if (masterVersion.build.isEmpty) { + nextBuildNumber = 1; + } else { + final int currentBuildNumber = masterVersion.build.first; + nextBuildNumber = currentBuildNumber + 1; + } + final Version preReleaseVersion = Version( + masterVersion.major, + masterVersion.minor, + masterVersion.patch, + build: nextBuildNumber.toString(), + ); + allowedNextVersions.clear(); + allowedNextVersions[masterVersion.nextMajor] = NextVersionType.RELEASE; + allowedNextVersions[masterVersion.nextMinor] = + NextVersionType.BREAKING_MAJOR; + allowedNextVersions[masterVersion.nextPatch] = NextVersionType.MINOR; + allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; + + final Version nextNullSafetyMajor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); + final Version nextNullSafetyMinor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextPatch); + + allowedNextVersions[nextNullSafetyMajor] = + NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE; + allowedNextVersions[nextNullSafetyMinor] = + NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE; + } + return allowedNextVersions; +} + +class VersionCheckCommand extends PluginCommand { + VersionCheckCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + this.gitDir, + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption(_kBaseSha); + } + + /// The git directory to use. By default it uses the parent directory. + /// + /// This can be mocked for testing. + final GitDir gitDir; + + @override + final String name = 'version-check'; + + @override + final String description = + 'Checks if the versions of the plugins have been incremented per pub specification.\n\n' + 'This command requires "pub" and "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + + final String rootDir = packagesDir.parent.absolute.path; + final String baseSha = argResults[_kBaseSha]; + + GitDir baseGitDir = gitDir; + if (baseGitDir == null) { + if (!await GitDir.isGitDir(rootDir)) { + print('$rootDir is not a valid Git repository.'); + throw ToolExit(2); + } + baseGitDir = await GitDir.fromExisting(rootDir); + } + + final GitVersionFinder gitVersionFinder = + GitVersionFinder(baseGitDir, baseSha); + + final List changedPubspecs = + await gitVersionFinder.getChangedPubSpecs(); + + for (final String pubspecPath in changedPubspecs) { + try { + final File pubspecFile = fileSystem.file(pubspecPath); + if (!pubspecFile.existsSync()) { + continue; + } + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec.publishTo == 'none') { + continue; + } + + final Version masterVersion = + await gitVersionFinder.getPackageVersion(pubspecPath, baseSha); + final Version headVersion = + await gitVersionFinder.getPackageVersion(pubspecPath, 'HEAD'); + if (headVersion == null) { + continue; // Example apps don't have versions + } + + final Map allowedNextVersions = + getAllowedNextVersions(masterVersion, headVersion); + + if (!allowedNextVersions.containsKey(headVersion)) { + final String error = '$pubspecPath incorrectly updated version.\n' + 'HEAD: $headVersion, master: $masterVersion.\n' + 'Allowed versions: $allowedNextVersions'; + final Colorize redError = Colorize(error)..red(); + print(redError); + throw ToolExit(1); + } + + bool isPlatformInterface = pubspec.name.endsWith("_platform_interface"); + if (isPlatformInterface && + allowedNextVersions[headVersion] == + NextVersionType.BREAKING_MAJOR) { + final String error = '$pubspecPath breaking change detected.\n' + 'Breaking changes to platform interfaces are strongly discouraged.\n'; + final Colorize redError = Colorize(error)..red(); + print(redError); + throw ToolExit(1); + } + } on io.ProcessException { + print('Unable to find pubspec in master for $pubspecPath.' + ' Safe to ignore if the project is new.'); + } + } + + print('No version check errors found!'); + } +} diff --git a/packages/flutter_plugin_tools/lib/src/xctest_command.dart b/packages/flutter_plugin_tools/lib/src/xctest_command.dart new file mode 100644 index 000000000000..d90b7a8fbfea --- /dev/null +++ b/packages/flutter_plugin_tools/lib/src/xctest_command.dart @@ -0,0 +1,216 @@ +// 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 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +const String _kiOSDestination = 'ios-destination'; +const String _kTarget = 'target'; +const String _kSkip = 'skip'; +const String _kXcodeBuildCommand = 'xcodebuild'; +const String _kXCRunCommand = 'xcrun'; +const String _kFoundNoSimulatorsMessage = + 'Cannot find any available simulators, tests failed'; + +/// The command to run iOS' XCTests in plugins, this should work for both XCUnitTest and XCUITest targets. +/// The tests target have to be added to the xcode project of the example app. Usually at "example/ios/Runner.xcodeproj". +/// The command takes a "-target" argument which has to match the target of the test target. +/// For information on how to add test target in an xcode project, see https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/UnitTesting.html +class XCTestCommand extends PluginCommand { + XCTestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + _kiOSDestination, + help: + 'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n' + 'this is passed to the `-destination` argument in xcodebuild command.\n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', + ); + argParser.addOption(_kTarget, + help: 'The test target.\n' + 'This is the xcode project test target. This is passed to the `-scheme` argument in the xcodebuild command. \n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the scheme'); + argParser.addMultiOption(_kSkip, + help: 'Plugins to skip while running this command. \n'); + } + + @override + final String name = 'xctest'; + + @override + final String description = 'Runs the xctests in the iOS example apps.\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + if (argResults[_kTarget] == null) { + // TODO(cyanglaz): Automatically find all the available testing schemes if this argument is not specified. + // https://github.com/flutter/flutter/issues/68419 + print('--$_kTarget must be specified'); + throw ToolExit(1); + } + + String destination = argResults[_kiOSDestination]; + if (destination == null) { + String simulatorId = await _findAvailableIphoneSimulator(); + if (simulatorId == null) { + print(_kFoundNoSimulatorsMessage); + throw ToolExit(1); + } + destination = 'id=$simulatorId'; + } + + checkSharding(); + + final String target = argResults[_kTarget]; + final List skipped = argResults[_kSkip]; + + List failingPackages = []; + await for (Directory plugin in getPlugins()) { + // Start running for package. + final String packageName = + p.relative(plugin.path, from: packagesDir.path); + print('Start running for $packageName ...'); + if (!isIosPlugin(plugin, fileSystem)) { + print('iOS is not supported by this plugin.'); + print('\n\n'); + continue; + } + if (skipped.contains(packageName)) { + print('$packageName was skipped with the --skip flag.'); + print('\n\n'); + continue; + } + for (Directory example in getExamplesForPlugin(plugin)) { + // Look for the test scheme in the example app. + print('Look for target named: $_kTarget ...'); + final List findSchemeArgs = [ + '-project', + 'ios/Runner.xcodeproj', + '-list', + '-json' + ]; + final String completeFindSchemeCommand = + '$_kXcodeBuildCommand ${findSchemeArgs.join(' ')}'; + print(completeFindSchemeCommand); + final io.ProcessResult xcodeprojListResult = await processRunner + .run(_kXcodeBuildCommand, findSchemeArgs, workingDir: example); + if (xcodeprojListResult.exitCode != 0) { + print('Error occurred while running "$completeFindSchemeCommand":\n' + '${xcodeprojListResult.stderr}'); + failingPackages.add(packageName); + print('\n\n'); + continue; + } + + final String xcodeprojListOutput = xcodeprojListResult.stdout; + Map xcodeprojListOutputJson = + jsonDecode(xcodeprojListOutput); + if (!xcodeprojListOutputJson['project']['targets'].contains(target)) { + failingPackages.add(packageName); + print('$target not configured for $packageName, test failed.'); + print( + 'Please check the scheme for the test target if it matches the name $target.\n' + 'If this plugin does not have an XCTest target, use the $_kSkip flag in the $name command to skip the plugin.'); + print('\n\n'); + continue; + } + // Found the scheme, running tests + print('Running XCTests:$target for $packageName ...'); + final List xctestArgs = [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + target, + '-destination', + destination, + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ]; + final String completeTestCommand = + '$_kXcodeBuildCommand ${xctestArgs.join(' ')}'; + print(completeTestCommand); + final int exitCode = await processRunner + .runAndStream(_kXcodeBuildCommand, xctestArgs, workingDir: example); + if (exitCode == 0) { + print('Successfully ran xctest for $packageName'); + } else { + failingPackages.add(packageName); + } + } + } + + // Command end, print reports. + if (failingPackages.isEmpty) { + print("All XCTests have passed!"); + } else { + print( + 'The following packages are failing XCTests (see above for details):'); + for (String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + } + + Future _findAvailableIphoneSimulator() async { + // Find the first available destination if not specified. + final List findSimulatorsArguments = [ + 'simctl', + 'list', + '--json' + ]; + final String findSimulatorCompleteCommand = + '$_kXCRunCommand ${findSimulatorsArguments.join(' ')}'; + print('Looking for available simulators...'); + print(findSimulatorCompleteCommand); + final io.ProcessResult findSimulatorsResult = + await processRunner.run(_kXCRunCommand, findSimulatorsArguments); + if (findSimulatorsResult.exitCode != 0) { + print('Error occurred while running "$findSimulatorCompleteCommand":\n' + '${findSimulatorsResult.stderr}'); + throw ToolExit(1); + } + final Map simulatorListJson = + jsonDecode(findSimulatorsResult.stdout); + final List runtimes = simulatorListJson['runtimes']; + final Map devices = simulatorListJson['devices']; + if (runtimes.isEmpty || devices.isEmpty) { + return null; + } + String id; + // Looking for runtimes, trying to find one with highest OS version. + for (Map runtimeMap in runtimes.reversed) { + if (!runtimeMap['name'].contains('iOS')) { + continue; + } + final String runtimeID = runtimeMap['identifier']; + final List devicesForRuntime = devices[runtimeID]; + if (devicesForRuntime.isEmpty) { + continue; + } + // Looking for runtimes, trying to find latest version of device. + for (Map device in devicesForRuntime.reversed) { + if (device['availabilityError'] != null || + (device['isAvailable'] as bool == false)) { + continue; + } + id = device['udid']; + print('device selected: $device'); + return id; + } + } + return null; + } +} diff --git a/packages/flutter_plugin_tools/pubspec.yaml b/packages/flutter_plugin_tools/pubspec.yaml new file mode 100644 index 000000000000..f8b452910236 --- /dev/null +++ b/packages/flutter_plugin_tools/pubspec.yaml @@ -0,0 +1,30 @@ +name: flutter_plugin_tools +description: Productivity utils for hosting multiple plugins within one repository. +homepage: https://github.com/flutter/plugin_tools +version: 0.0.45+1 + +dependencies: + args: "^1.4.3" + path: "^1.6.1" + http: "^0.12.1" + async: "^2.0.7" + yaml: "^2.1.15" + quiver: "^2.0.2" + pub_semver: ^1.4.2 + colorize: ^2.0.0 + git: ^1.0.0 + platform: ^2.2.0 + pubspec_parse: "^0.1.4" + test: ^1.6.4 + meta: ^1.1.7 + file: ^5.0.10 + uuid: ^2.0.4 + http_multi_server: ^2.2.0 + +dev_dependencies: + matcher: ^0.12.6 + mockito: ^4.1.1 + pedantic: 1.8.0 + +environment: + sdk: ">=2.3.0 <3.0.0" diff --git a/packages/flutter_plugin_tools/test/analyze_command_test.dart b/packages/flutter_plugin_tools/test/analyze_command_test.dart new file mode 100644 index 000000000000..9e7a42bbb680 --- /dev/null +++ b/packages/flutter_plugin_tools/test/analyze_command_test.dart @@ -0,0 +1,93 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/analyze_command.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + RecordingProcessRunner processRunner; + CommandRunner runner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final AnalyzeCommand analyzeCommand = AnalyzeCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('analyze_command', 'Test for analyze_command'); + runner.addCommand(analyzeCommand); + }); + + tearDown(() { + mockPackagesDir.deleteSync(recursive: true); + }); + + test('analyzes all packages', () async { + final Directory plugin1Dir = await createFakePlugin('a'); + final Directory plugin2Dir = await createFakePlugin('b'); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('pub', ['global', 'activate', 'tuneup'], + mockPackagesDir.path), + ProcessCall('flutter', ['packages', 'get'], plugin1Dir.path), + ProcessCall('flutter', ['packages', 'get'], plugin2Dir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + plugin1Dir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + plugin2Dir.path), + ])); + }); + + group('verifies analysis settings', () { + test('fails analysis_options.yaml', () async { + await createFakePlugin('foo', withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + await expectLater(() => runner.run(['analyze']), + throwsA(const TypeMatcher())); + }); + + test('fails .analysis_options', () async { + await createFakePlugin('foo', withExtraFiles: >[ + ['.analysis_options'] + ]); + + await expectLater(() => runner.run(['analyze']), + throwsA(const TypeMatcher())); + }); + + test('takes an allow list', () async { + final Directory pluginDir = + await createFakePlugin('foo', withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze', '--custom-analysis', 'foo']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('pub', ['global', 'activate', 'tuneup'], + mockPackagesDir.path), + ProcessCall('flutter', ['packages', 'get'], pluginDir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + pluginDir.path), + ])); + }); + }); +} diff --git a/packages/flutter_plugin_tools/test/build_examples_command_test.dart b/packages/flutter_plugin_tools/test/build_examples_command_test.dart new file mode 100644 index 000000000000..eaf5049dcc02 --- /dev/null +++ b/packages/flutter_plugin_tools/test/build_examples_command_test.dart @@ -0,0 +1,470 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/build_examples_command.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test build_example_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final BuildExamplesCommand command = BuildExamplesCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner( + 'build_examples_command', 'Test for build_example_command'); + runner.addCommand(command); + cleanupPackages(); + }); + + test('building for iOS when plugin is not set up for iOS results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ipa', '--no-macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING IPA for $packageName', + 'iOS is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for ios', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'build-examples', + '--ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING IPA for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Linux when plugin is not set up for Linux results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--linux']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Linux for $packageName', + 'Linux is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --linux with no + // Linux implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for Linux', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--linux']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Linux for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'linux'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('building for macos with no implementation results in no-op', + () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['example', 'test'], + ]); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING macOS for $packageName', + '\macOS is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + test('building for macos', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ['example', 'macos', 'macos.swift'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING macOS for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['pub', 'get'], + pluginExampleDirectory.path), + ProcessCall(flutterCommand, ['build', 'macos'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Windows when plugin is not set up for Windows results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isWindowsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--windows']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Windows for $packageName', + 'Windows is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for windows', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isWindowsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--windows']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Windows for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'windows'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Android when plugin is not set up for Android results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--apk', '--no-ipa']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING APK for $packageName', + 'Android is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for android', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'build-examples', + '--apk', + '--no-ipa', + '--no-macos', + ]); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING APK for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'apk'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('enable-experiment flag for Android', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'build-examples', + '--apk', + '--no-ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + ['build', 'apk', '--enable-experiment=exp1'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('enable-experiment flag for ios', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'build-examples', + '--ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + }); +} diff --git a/packages/flutter_plugin_tools/test/common_test.dart b/packages/flutter_plugin_tools/test/common_test.dart new file mode 100644 index 000000000000..b3504c2358d9 --- /dev/null +++ b/packages/flutter_plugin_tools/test/common_test.dart @@ -0,0 +1,100 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + RecordingProcessRunner processRunner; + CommandRunner runner; + List plugins; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + plugins = []; + final SamplePluginCommand samplePluginCommand = SamplePluginCommand( + plugins, + mockPackagesDir, + mockFileSystem, + processRunner: processRunner, + ); + runner = + CommandRunner('common_command', 'Test for common functionality'); + runner.addCommand(samplePluginCommand); + }); + + tearDown(() { + mockPackagesDir.deleteSync(recursive: true); + }); + + test('all plugins from file system', () async { + final Directory plugin1 = createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run(['sample']); + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('exclude plugins when plugins flag is specified', () async { + createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run( + ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); + expect(plugins, unorderedEquals([plugin2.path])); + }); + + test('exclude plugins when plugins flag isn\'t specified', () async { + createFakePlugin('plugin1'); + createFakePlugin('plugin2'); + await runner.run(['sample', '--exclude=plugin1,plugin2']); + expect(plugins, unorderedEquals([])); + }); + + test('exclude federated plugins when plugins flag is specified', () async { + createFakePlugin('plugin1', parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run([ + 'sample', + '--plugins=federated/plugin1,plugin2', + '--exclude=federated/plugin1' + ]); + expect(plugins, unorderedEquals([plugin2.path])); + }); + + test('exclude entire federated plugins when plugins flag is specified', + () async { + createFakePlugin('plugin1', parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run([ + 'sample', + '--plugins=federated/plugin1,plugin2', + '--exclude=federated' + ]); + expect(plugins, unorderedEquals([plugin2.path])); + }); +} + +class SamplePluginCommand extends PluginCommand { + SamplePluginCommand( + this.plugins_, + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner); + + List plugins_; + + @override + final String name = 'sample'; + + @override + final String description = 'sample command'; + + @override + Future run() async { + await for (Directory package in getPlugins()) { + this.plugins_.add(package.path); + } + } +} diff --git a/packages/flutter_plugin_tools/test/drive_examples_command_test.dart b/packages/flutter_plugin_tools/test/drive_examples_command_test.dart new file mode 100644 index 000000000000..f4bdd95c1664 --- /dev/null +++ b/packages/flutter_plugin_tools/test/drive_examples_command_test.dart @@ -0,0 +1,505 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test drive_example_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final DriveExamplesCommand command = DriveExamplesCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner( + 'drive_examples_command', 'Test for drive_example_command'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('driving under folder "test"', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], + isIosPlugin: true, + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving under folder "test_driver"', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving under folder "test_driver" when test files are missing"', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await expectLater( + () => runCapturingPrint(runner, ['drive-examples']), + throwsA(const TypeMatcher())); + }); + + test( + 'driving under folder "test_driver" when targets are under "integration_test"', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'integration_test.dart'], + ['example', 'integration_test', 'bar_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'ignore_me.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String driverTestPath = p.join('test_driver', 'integration_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'bar_test.dart'), + ], + pluginExampleDirectory.path), + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'foo_test.dart'), + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support Linux is a no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --linux on a non-Linux + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Linux plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isLinuxPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'linux', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport macOS is a no-op', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ]); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + test('driving on a macOS plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ['example', 'macos', 'macos.swift'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'macos', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport windows is a no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --windows on a non-windows + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Windows plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isWindowsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'windows', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support mobile is no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('enable-experiment flag', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], + isIosPlugin: true, + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'drive-examples', + '--enable-experiment=exp1', + ]); + + String deviceTestPath = p.join('test', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--enable-experiment=exp1', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + }); +} diff --git a/packages/flutter_plugin_tools/test/firebase_test_lab_test.dart b/packages/flutter_plugin_tools/test/firebase_test_lab_test.dart new file mode 100644 index 000000000000..97b977619d57 --- /dev/null +++ b/packages/flutter_plugin_tools/test/firebase_test_lab_test.dart @@ -0,0 +1,256 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$FirebaseTestLabCommand', () { + final List printedMessages = []; + CommandRunner runner; + RecordingProcessRunner processRunner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final FirebaseTestLabCommand command = FirebaseTestLabCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, + print: (Object message) => printedMessages.add(message.toString())); + + runner = CommandRunner( + 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); + runner.addCommand(command); + }); + + tearDown(() { + printedMessages.clear(); + }); + + test('retries gcloud set', () async { + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(1); + processRunner.processToReturn = mockProcess; + createFakePlugin('plugin', withExtraFiles: >[ + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + await expectLater( + () => runCapturingPrint(runner, ['firebase-test-lab']), + throwsA(const TypeMatcher())); + expect( + printedMessages, + contains( + "\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.")); + }); + + test('runs e2e tests', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['test', 'plugin_test.dart'], + ['test', 'plugin_e2e.dart'], + ['should_not_run_e2e.dart'], + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'should_not_run.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + ]); + + expect( + printedMessages, + orderedEquals([ + '\nRUNNING FIREBASE TEST LAB TESTS for plugin', + '\nFirebase project configured.', + '\n\n', + 'All Firebase Test Lab tests successful!', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ]), + ); + }); + + test('experimental flag', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['test', 'plugin_test.dart'], + ['test', 'plugin_e2e.dart'], + ['should_not_run_e2e.dart'], + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'should_not_run.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + + await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--test-run-id', + 'testRunId', + '--enable-experiment=exp1', + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ]), + ); + + cleanupPackages(); + }); + }); +} diff --git a/packages/flutter_plugin_tools/test/lint_podspecs_command_test.dart b/packages/flutter_plugin_tools/test/lint_podspecs_command_test.dart new file mode 100644 index 000000000000..49d6ad4d8e20 --- /dev/null +++ b/packages/flutter_plugin_tools/test/lint_podspecs_command_test.dart @@ -0,0 +1,202 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$LintPodspecsCommand', () { + CommandRunner runner; + MockPlatform mockPlatform; + final RecordingProcessRunner processRunner = RecordingProcessRunner(); + List printedMessages; + + setUp(() { + initializeFakePackages(); + + printedMessages = []; + mockPlatform = MockPlatform(); + when(mockPlatform.isMacOS).thenReturn(true); + final LintPodspecsCommand command = LintPodspecsCommand( + mockPackagesDir, + mockFileSystem, + processRunner: processRunner, + platform: mockPlatform, + print: (Object message) => printedMessages.add(message.toString()), + ); + + runner = + CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); + runner.addCommand(command); + final MockProcess mockLintProcess = MockProcess(); + mockLintProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockLintProcess; + processRunner.recordedCalls.clear(); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('only runs on macOS', () async { + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + when(mockPlatform.isMacOS).thenReturn(false); + await runner.run(['podspecs']); + + expect( + processRunner.recordedCalls, + equals([]), + ); + }); + + test('runs pod lib lint on a podspec', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['ios', 'plugin1.podspec'], + ['bogus.dart'], // Ignore non-podspecs. + ]); + + processRunner.resultStdout = 'Foo'; + processRunner.resultStderr = 'Bar'; + + await runner.run(['podspecs']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--analyze', + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--analyze', + ], + mockPackagesDir.path), + ]), + ); + + expect( + printedMessages, contains('Linting and analyzing plugin1.podspec')); + expect(printedMessages, contains('Foo')); + expect(printedMessages, contains('Bar')); + }); + + test('skips podspecs with known issues', () async { + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'] + ]); + createFakePlugin('plugin2', withExtraFiles: >[ + ['plugin2.podspec'] + ]); + + await runner + .run(['podspecs', '--skip=plugin1', '--skip=plugin2']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ]), + ); + }); + + test('skips analyzer for podspecs with known warnings', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + await runner.run(['podspecs', '--no-analyze=plugin1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + ], + mockPackagesDir.path), + ]), + ); + + expect(printedMessages, contains('Linting plugin1.podspec')); + }); + + test('allow warnings for podspecs with known warnings', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + await runner.run(['podspecs', '--ignore-warnings=plugin1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--allow-warnings', + '--analyze', + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--allow-warnings', + '--analyze', + ], + mockPackagesDir.path), + ]), + ); + + expect( + printedMessages, contains('Linting and analyzing plugin1.podspec')); + }); + }); +} + +class MockPlatform extends Mock implements Platform {} diff --git a/packages/flutter_plugin_tools/test/list_command_test.dart b/packages/flutter_plugin_tools/test/list_command_test.dart new file mode 100644 index 000000000000..478625283dd0 --- /dev/null +++ b/packages/flutter_plugin_tools/test/list_command_test.dart @@ -0,0 +1,198 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/list_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$ListCommand', () { + CommandRunner runner; + + setUp(() { + initializeFakePackages(); + final ListCommand command = ListCommand(mockPackagesDir, mockFileSystem); + + runner = CommandRunner('list_test', 'Test for $ListCommand'); + runner.addCommand(command); + }); + + test('lists plugins', () async { + createFakePlugin('plugin1'); + createFakePlugin('plugin2'); + + final List plugins = + await runCapturingPrint(runner, ['list', '--type=plugin']); + + expect( + plugins, + orderedEquals([ + '/packages/plugin1', + '/packages/plugin2', + ]), + ); + + cleanupPackages(); + }); + + test('lists examples', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=example']); + + expect( + examples, + orderedEquals([ + '/packages/plugin1/example', + '/packages/plugin2/example/example1', + '/packages/plugin2/example/example2', + ]), + ); + + cleanupPackages(); + }); + + test('lists packages', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List packages = + await runCapturingPrint(runner, ['list', '--type=package']); + + expect( + packages, + unorderedEquals([ + '/packages/plugin1', + '/packages/plugin1/example', + '/packages/plugin2', + '/packages/plugin2/example/example1', + '/packages/plugin2/example/example2', + '/packages/plugin3', + ]), + ); + + cleanupPackages(); + }); + + test('lists files', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=file']); + + expect( + examples, + unorderedEquals([ + '/packages/plugin1/pubspec.yaml', + '/packages/plugin1/example/pubspec.yaml', + '/packages/plugin2/pubspec.yaml', + '/packages/plugin2/example/example1/pubspec.yaml', + '/packages/plugin2/example/example2/pubspec.yaml', + '/packages/plugin3/pubspec.yaml', + ]), + ); + + cleanupPackages(); + }); + + test('lists plugins using federated plugin layout', () async { + createFakePlugin('plugin1'); + + // Create a federated plugin by creating a directory under the packages + // directory with several packages underneath. + final Directory federatedPlugin = + mockPackagesDir.childDirectory('my_plugin')..createSync(); + final Directory clientLibrary = + federatedPlugin.childDirectory('my_plugin')..createSync(); + createFakePubspec(clientLibrary); + final Directory webLibrary = + federatedPlugin.childDirectory('my_plugin_web')..createSync(); + createFakePubspec(webLibrary); + final Directory macLibrary = + federatedPlugin.childDirectory('my_plugin_macos')..createSync(); + createFakePubspec(macLibrary); + + // Test without specifying `--type`. + final List plugins = + await runCapturingPrint(runner, ['list']); + + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + '/packages/my_plugin/my_plugin', + '/packages/my_plugin/my_plugin_web', + '/packages/my_plugin/my_plugin_macos', + ]), + ); + + cleanupPackages(); + }); + + test('can filter plugins with the --plugins argument', () async { + createFakePlugin('plugin1'); + + // Create a federated plugin by creating a directory under the packages + // directory with several packages underneath. + final Directory federatedPlugin = + mockPackagesDir.childDirectory('my_plugin')..createSync(); + final Directory clientLibrary = + federatedPlugin.childDirectory('my_plugin')..createSync(); + createFakePubspec(clientLibrary); + final Directory webLibrary = + federatedPlugin.childDirectory('my_plugin_web')..createSync(); + createFakePubspec(webLibrary); + final Directory macLibrary = + federatedPlugin.childDirectory('my_plugin_macos')..createSync(); + createFakePubspec(macLibrary); + + List plugins = await runCapturingPrint( + runner, ['list', '--plugins=plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--plugins=my_plugin']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin', + '/packages/my_plugin/my_plugin_web', + '/packages/my_plugin/my_plugin_macos', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--plugins=my_plugin/my_plugin_web']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin_web', + ]), + ); + + plugins = await runCapturingPrint(runner, + ['list', '--plugins=my_plugin/my_plugin_web,plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + '/packages/my_plugin/my_plugin_web', + ]), + ); + }); + }); +} diff --git a/packages/flutter_plugin_tools/test/mocks.dart b/packages/flutter_plugin_tools/test/mocks.dart new file mode 100644 index 000000000000..3e17ff8efd32 --- /dev/null +++ b/packages/flutter_plugin_tools/test/mocks.dart @@ -0,0 +1,33 @@ +import 'dart:async'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:mockito/mockito.dart'; + +class MockProcess extends Mock implements io.Process { + final Completer exitCodeCompleter = Completer(); + final StreamController> stdoutController = + StreamController>(); + final StreamController> stderrController = + StreamController>(); + final MockIOSink stdinMock = MockIOSink(); + + @override + Future get exitCode => exitCodeCompleter.future; + + @override + Stream> get stdout => stdoutController.stream; + + @override + Stream> get stderr => stderrController.stream; + + @override + IOSink get stdin => stdinMock; +} + +class MockIOSink extends Mock implements IOSink { + List lines = []; + + @override + void writeln([Object obj = ""]) => lines.add(obj); +} diff --git a/packages/flutter_plugin_tools/test/publish_plugin_command_test.dart b/packages/flutter_plugin_tools/test/publish_plugin_command_test.dart new file mode 100644 index 000000000000..d01bfa67ce24 --- /dev/null +++ b/packages/flutter_plugin_tools/test/publish_plugin_command_test.dart @@ -0,0 +1,359 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:git/git.dart'; +import 'package:matcher/matcher.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + const String testPluginName = 'foo'; + final List printedMessages = []; + + Directory parentDir; + Directory pluginDir; + GitDir gitDir; + TestProcessRunner processRunner; + CommandRunner commandRunner; + MockStdin mockStdin; + + setUp(() async { + // This test uses a local file system instead of an in memory one throughout + // so that git actually works. In setup we initialize a mono repo of plugins + // with one package and commit everything to Git. + parentDir = const LocalFileSystem() + .systemTempDirectory + .createTempSync('publish_plugin_command_test-'); + initializeFakePackages(parentDir: parentDir); + pluginDir = createFakePlugin(testPluginName, withSingleExample: false); + assert(pluginDir != null && pluginDir.existsSync()); + createFakePubspec(pluginDir, includeVersion: true); + io.Process.runSync('git', ['init'], + workingDirectory: mockPackagesDir.path); + gitDir = await GitDir.fromExisting(mockPackagesDir.path); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Initial commit']); + processRunner = TestProcessRunner(); + mockStdin = MockStdin(); + commandRunner = CommandRunner('tester', '') + ..addCommand(PublishPluginCommand( + mockPackagesDir, const LocalFileSystem(), + processRunner: processRunner, + print: (Object message) => printedMessages.add(message.toString()), + stdinput: mockStdin)); + }); + + tearDown(() { + parentDir.deleteSync(recursive: true); + printedMessages.clear(); + }); + + group('Initial validation', () { + test('requires a package flag', () async { + await expectLater(() => commandRunner.run(['publish-plugin']), + throwsA(const TypeMatcher())); + + expect( + printedMessages.last, contains("Must specify a package to publish.")); + }); + + test('requires an existing flag', () async { + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', 'iamerror']), + throwsA(const TypeMatcher())); + + expect(printedMessages.last, contains('iamerror does not exist')); + }); + + test('refuses to proceed with dirty files', () async { + pluginDir.childFile('tmp').createSync(); + + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', testPluginName]), + throwsA(const TypeMatcher())); + + expect( + printedMessages.last, + contains( + "There are files in the package directory that haven't been saved in git.")); + }); + + test('fails immediately if the remote doesn\'t exist', () async { + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', testPluginName]), + throwsA(const TypeMatcher())); + + expect(processRunner.results.last.stderr, contains("No such remote")); + }); + + test("doesn't validate the remote if it's not pushing tags", () async { + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + + expect(printedMessages.last, 'Done!'); + }); + }); + + group('Publishes package', () { + test('while showing all output from pub publish to the user', () async { + final Future publishCommand = commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + processRunner.mockPublishProcess.stdoutController.add(utf8.encode('Foo')); + processRunner.mockPublishProcess.stderrController.add(utf8.encode('Bar')); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await publishCommand; + + expect(printedMessages, contains('Foo')); + expect(printedMessages, contains('Bar')); + }); + + test('forwards input from the user to `pub publish`', () async { + final Future publishCommand = commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + mockStdin.controller.add(utf8.encode('user input')); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await publishCommand; + + expect(processRunner.mockPublishProcess.stdinMock.lines, + contains('user input')); + }); + + test('forwards --pub-publish-flags to pub publish', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + '--pub-publish-flags', + '--dry-run,--server=foo' + ]); + + expect(processRunner.mockPublishArgs.length, 4); + expect(processRunner.mockPublishArgs[0], 'pub'); + expect(processRunner.mockPublishArgs[1], 'publish'); + expect(processRunner.mockPublishArgs[2], '--dry-run'); + expect(processRunner.mockPublishArgs[3], '--server=foo'); + }); + + test('throws if pub publish fails', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(128); + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains("Publish failed. Exiting.")); + }); + }); + + group('Tags release', () { + test('with the version and name from the pubspec.yaml', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ]); + + final String tag = + (await gitDir.runCommand(['show-ref', 'fake_package-v0.0.1'])) + .stdout; + expect(tag, isNotEmpty); + }); + + test('only if publishing succeeded', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(128); + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains("Publish failed. Exiting.")); + final String tag = (await gitDir.runCommand( + ['show-ref', 'fake_package-v0.0.1'], + throwOnError: false)) + .stdout; + expect(tag, isEmpty); + }); + }); + + group('Pushes tags', () { + setUp(() async { + await gitDir.runCommand( + ['remote', 'add', 'upstream', 'http://localhost:8000']); + }); + + test('requires user confirmation', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'help'; + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains('Tag push canceled.')); + }); + + test('to upstream by default', () async { + await gitDir.runCommand(['tag', 'garbage']); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + + test('to different remotes based on a flag', () async { + await gitDir.runCommand( + ['remote', 'add', 'origin', 'http://localhost:8001']); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--remote', + 'origin', + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'origin'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + + test('only if tagging and pushing to remotes are both enabled', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-tag-release', + ]); + + expect(processRunner.pushTagsArgs.isEmpty, isTrue); + expect(printedMessages.last, 'Done!'); + }); + }); +} + +class TestProcessRunner extends ProcessRunner { + final List results = []; + final MockProcess mockPublishProcess = MockProcess(); + final List mockPublishArgs = []; + final MockProcessResult mockPushTagsResult = MockProcessResult(); + final List pushTagsArgs = []; + + @override + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + // Don't ever really push tags. + if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { + pushTagsArgs.addAll(args); + return mockPushTagsResult; + } + + final io.ProcessResult result = io.Process.runSync(executable, args, + workingDirectory: workingDir?.path); + results.add(result); + if (result.exitCode != 0) { + throw ToolExit(result.exitCode); + } + return result; + } + + @override + Future start(String executable, List args, + {Directory workingDirectory}) async { + /// Never actually publish anything. Start is always and only used for this + /// since it returns something we can route stdin through. + assert(executable == 'flutter' && + args.isNotEmpty && + args[0] == 'pub' && + args[1] == 'publish'); + mockPublishArgs.addAll(args); + return mockPublishProcess; + } +} + +class MockStdin extends Mock implements io.Stdin { + final StreamController> controller = StreamController>(); + String readLineOutput; + + @override + Stream transform(StreamTransformer streamTransformer) { + return controller.stream.transform(streamTransformer); + } + + @override + StreamSubscription> listen(void onData(List event), + {Function onError, void onDone(), bool cancelOnError}) { + return controller.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + String readLineSync( + {Encoding encoding = io.systemEncoding, + bool retainNewlines = false}) => + readLineOutput; +} + +class MockProcessResult extends Mock implements io.ProcessResult {} diff --git a/packages/flutter_plugin_tools/test/test_command_test.dart b/packages/flutter_plugin_tools/test/test_command_test.dart new file mode 100644 index 000000000000..514e4c27190a --- /dev/null +++ b/packages/flutter_plugin_tools/test/test_command_test.dart @@ -0,0 +1,154 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/test_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$TestCommand', () { + CommandRunner runner; + final RecordingProcessRunner processRunner = RecordingProcessRunner(); + + setUp(() { + initializeFakePackages(); + final TestCommand command = TestCommand(mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('test_test', 'Test for $TestCommand'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + processRunner.recordedCalls.clear(); + }); + + test('runs flutter test on each plugin', () async { + final Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = + createFakePlugin('plugin2', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', ['test', '--color'], plugin1Dir.path), + ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('skips testing plugins without test directory', () async { + createFakePlugin('plugin1'); + final Directory plugin2Dir = + createFakePlugin('plugin2', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('runs pub run test on non-Flutter packages', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', + isFlutter: true, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', + isFlutter: false, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + ['test', '--color', '--enable-experiment=exp1'], + plugin1Dir.path), + ProcessCall('pub', ['get'], plugin2Dir.path), + ProcessCall( + 'pub', + ['run', '--enable-experiment=exp1', 'test'], + plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('runs on Chrome for web plugins', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ], + isFlutter: true, + isWebPlugin: true, + ); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', + ['test', '--color', '--platform=chrome'], pluginDir.path), + ]), + ); + }); + + test('enable-experiment flag', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', + isFlutter: true, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', + isFlutter: false, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + ['test', '--color', '--enable-experiment=exp1'], + plugin1Dir.path), + ProcessCall('pub', ['get'], plugin2Dir.path), + ProcessCall( + 'pub', + ['run', '--enable-experiment=exp1', 'test'], + plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + }); +} diff --git a/packages/flutter_plugin_tools/test/util.dart b/packages/flutter_plugin_tools/test/util.dart new file mode 100644 index 000000000000..ec0000d13f34 --- /dev/null +++ b/packages/flutter_plugin_tools/test/util.dart @@ -0,0 +1,291 @@ +import 'dart:async'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:platform/platform.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:quiver/collection.dart'; + +FileSystem mockFileSystem = MemoryFileSystem( + style: LocalPlatform().isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix); +Directory mockPackagesDir; + +/// Creates a mock packages directory in the mock file system. +/// +/// If [parentDir] is set the mock packages dir will be creates as a child of +/// it. If not [mockFileSystem] will be used instead. +void initializeFakePackages({Directory parentDir}) { + mockPackagesDir = + (parentDir ?? mockFileSystem.currentDirectory).childDirectory('packages'); + mockPackagesDir.createSync(); +} + +/// Creates a plugin package with the given [name] in [mockPackagesDir]. +Directory createFakePlugin( + String name, { + bool withSingleExample = false, + List withExamples = const [], + List> withExtraFiles = const >[], + bool isFlutter = true, + bool isAndroidPlugin = false, + bool isIosPlugin = false, + bool isWebPlugin = false, + bool isLinuxPlugin = false, + bool isMacOsPlugin = false, + bool isWindowsPlugin = false, + String parentDirectoryName = '', +}) { + assert(!(withSingleExample && withExamples.isNotEmpty), + 'cannot pass withSingleExample and withExamples simultaneously'); + + final Directory pluginDirectory = (parentDirectoryName != '') + ? mockPackagesDir.childDirectory(parentDirectoryName).childDirectory(name) + : mockPackagesDir.childDirectory(name); + pluginDirectory.createSync(recursive: true); + + createFakePubspec( + pluginDirectory, + name: name, + isFlutter: isFlutter, + isAndroidPlugin: isAndroidPlugin, + isIosPlugin: isIosPlugin, + isWebPlugin: isWebPlugin, + isLinuxPlugin: isLinuxPlugin, + isMacOsPlugin: isMacOsPlugin, + isWindowsPlugin: isWindowsPlugin, + ); + + if (withSingleExample) { + final Directory exampleDir = pluginDirectory.childDirectory('example') + ..createSync(); + createFakePubspec(exampleDir, + name: "${name}_example", isFlutter: isFlutter); + } else if (withExamples.isNotEmpty) { + final Directory exampleDir = pluginDirectory.childDirectory('example') + ..createSync(); + for (String example in withExamples) { + final Directory currentExample = exampleDir.childDirectory(example) + ..createSync(); + createFakePubspec(currentExample, name: example, isFlutter: isFlutter); + } + } + + for (List file in withExtraFiles) { + final List newFilePath = [pluginDirectory.path] + ..addAll(file); + final File newFile = + mockFileSystem.file(mockFileSystem.path.joinAll(newFilePath)); + newFile.createSync(recursive: true); + } + + return pluginDirectory; +} + +/// Creates a `pubspec.yaml` file with a flutter dependency. +void createFakePubspec( + Directory parent, { + String name = 'fake_package', + bool isFlutter = true, + bool includeVersion = false, + bool isAndroidPlugin = false, + bool isIosPlugin = false, + bool isWebPlugin = false, + bool isLinuxPlugin = false, + bool isMacOsPlugin = false, + bool isWindowsPlugin = false, +}) { + parent.childFile('pubspec.yaml').createSync(); + String yaml = ''' +name: $name +flutter: + plugin: + platforms: +'''; + if (isAndroidPlugin) { + yaml += ''' + android: + package: io.flutter.plugins.fake + pluginClass: FakePlugin +'''; + } + if (isIosPlugin) { + yaml += ''' + ios: + pluginClass: FLTFakePlugin +'''; + } + if (isWebPlugin) { + yaml += ''' + web: + pluginClass: FakePlugin + fileName: ${name}_web.dart +'''; + } + if (isLinuxPlugin) { + yaml += ''' + linux: + pluginClass: FakePlugin +'''; + } + if (isMacOsPlugin) { + yaml += ''' + macos: + pluginClass: FakePlugin +'''; + } + if (isWindowsPlugin) { + yaml += ''' + windows: + pluginClass: FakePlugin +'''; + } + if (isFlutter) { + yaml += ''' +dependencies: + flutter: + sdk: flutter +'''; + } + if (includeVersion) { + yaml += ''' +version: 0.0.1 +publish_to: none # Hardcoded safeguard to prevent this from somehow being published by a broken test. +'''; + } + parent.childFile('pubspec.yaml').writeAsStringSync(yaml); +} + +/// Cleans up the mock packages directory, making it an empty directory again. +void cleanupPackages() { + mockPackagesDir.listSync().forEach((FileSystemEntity entity) { + entity.deleteSync(recursive: true); + }); +} + +/// Run the command [runner] with the given [args] and return +/// what was printed. +Future> runCapturingPrint( + CommandRunner runner, List args) async { + final List prints = []; + final ZoneSpecification spec = ZoneSpecification( + print: (_, __, ___, String message) { + prints.add(message); + }, + ); + await Zone.current + .fork(specification: spec) + .run>(() => runner.run(args)); + + return prints; +} + +/// A mock [ProcessRunner] which records process calls. +class RecordingProcessRunner extends ProcessRunner { + io.Process processToReturn; + final List recordedCalls = []; + + /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. + String resultStdout; + + /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. + String resultStderr; + + @override + Future runAndStream( + String executable, + List args, { + Directory workingDir, + bool exitOnError = false, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + return Future.value( + processToReturn == null ? 0 : await processToReturn.exitCode); + } + + /// Returns [io.ProcessResult] created from [processToReturn], [resultStdout], and [resultStderr]. + @override + Future run(String executable, List args, + {Directory workingDir, + bool exitOnError = false, + stdoutEncoding = io.systemEncoding, + stderrEncoding = io.systemEncoding}) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + io.ProcessResult result; + + if (processToReturn != null) { + result = io.ProcessResult( + processToReturn.pid, + await processToReturn.exitCode, + resultStdout ?? processToReturn.stdout, + resultStderr ?? processToReturn.stderr); + } + return Future.value(result); + } + + @override + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + io.ProcessResult result; + if (processToReturn != null) { + result = io.ProcessResult( + processToReturn.pid, + await processToReturn.exitCode, + resultStdout ?? processToReturn.stdout, + resultStderr ?? processToReturn.stderr); + } + return Future.value(result); + } + + @override + Future start(String executable, List args, + {Directory workingDirectory}) async { + recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); + return Future.value(processToReturn); + } +} + +/// A recorded process call. +class ProcessCall { + const ProcessCall(this.executable, this.args, this.workingDir); + + /// The executable that was called. + final String executable; + + /// The arguments passed to [executable] in the call. + final List args; + + /// The working directory this process was called from. + final String workingDir; + + @override + bool operator ==(dynamic other) { + if (other is! ProcessCall) { + return false; + } + final ProcessCall otherCall = other; + return executable == otherCall.executable && + listsEqual(args, otherCall.args) && + workingDir == otherCall.workingDir; + } + + @override + int get hashCode => + executable?.hashCode ?? + 0 ^ args?.hashCode ?? + 0 ^ workingDir?.hashCode ?? + 0; + + @override + String toString() { + final List command = [executable]..addAll(args); + return '"${command.join(' ')}" in $workingDir'; + } +} diff --git a/packages/flutter_plugin_tools/test/version_check_test.dart b/packages/flutter_plugin_tools/test/version_check_test.dart new file mode 100644 index 000000000000..b9ace3811bff --- /dev/null +++ b/packages/flutter_plugin_tools/test/version_check_test.dart @@ -0,0 +1,319 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:git/git.dart'; +import 'package:mockito/mockito.dart'; +import "package:test/test.dart"; +import "package:flutter_plugin_tools/src/version_check_command.dart"; +import 'package:pub_semver/pub_semver.dart'; +import 'util.dart'; + +void testAllowedVersion( + String masterVersion, + String headVersion, { + bool allowed = true, + NextVersionType nextVersionType, +}) { + final Version master = Version.parse(masterVersion); + final Version head = Version.parse(headVersion); + final Map allowedVersions = + getAllowedNextVersions(master, head); + if (allowed) { + expect(allowedVersions, contains(head)); + if (nextVersionType != null) { + expect(allowedVersions[head], equals(nextVersionType)); + } + } else { + expect(allowedVersions, isNot(contains(head))); + } +} + +class MockGitDir extends Mock implements GitDir {} + +class MockProcessResult extends Mock implements ProcessResult {} + +void main() { + group('$VersionCheckCommand', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + List> gitDirCommands; + String gitDiffResponse; + Map gitShowResponses; + + setUp(() { + gitDirCommands = >[]; + gitDiffResponse = ''; + gitShowResponses = {}; + final MockGitDir gitDir = MockGitDir(); + when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { + gitDirCommands.add(invocation.positionalArguments[0]); + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'diff') { + when(mockProcessResult.stdout).thenReturn(gitDiffResponse); + } else if (invocation.positionalArguments[0][0] == 'show') { + final String response = + gitShowResponses[invocation.positionalArguments[0][1]]; + when(mockProcessResult.stdout).thenReturn(response); + } + return Future.value(mockProcessResult); + }); + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final VersionCheckCommand command = VersionCheckCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, gitDir: gitDir); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('allows valid version', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect(gitDirCommands[1].join(' '), + equals('show master:packages/plugin/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin/pubspec.yaml')); + }); + + test('denies invalid version', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', + }; + final Future> result = runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect(gitDirCommands[1].join(' '), + equals('show master:packages/plugin/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin/pubspec.yaml')); + }); + + test('gracefully handles missing pubspec.yaml', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + mockFileSystem.currentDirectory + .childDirectory('packages') + .childDirectory('plugin') + .childFile('pubspec.yaml') + .deleteSync(); + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(1)); + expect(gitDirCommands.first.join(' '), + equals('diff --name-only master HEAD')); + }); + + test('allows minor changes to platform interfaces', () async { + createFakePlugin('plugin_platform_interface'); + gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.1.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect( + gitDirCommands[1].join(' '), + equals( + 'show master:packages/plugin_platform_interface/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); + }); + + test('disallows breaking changes to platform interfaces', () async { + createFakePlugin('plugin_platform_interface'); + gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml': + 'version: 2.0.0', + }; + final Future> output = runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + await expectLater( + output, + throwsA(const TypeMatcher()), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect( + gitDirCommands[1].join(' '), + equals( + 'show master:packages/plugin_platform_interface/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); + }); + }); + + group("Pre 1.0", () { + test("nextVersion allows patch version", () { + testAllowedVersion("0.12.0", "0.12.0+1", + nextVersionType: NextVersionType.PATCH); + testAllowedVersion("0.12.0+4", "0.12.0+5", + nextVersionType: NextVersionType.PATCH); + }); + + test("nextVersion does not allow jumping patch", () { + testAllowedVersion("0.12.0", "0.12.0+2", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.0+4", allowed: false); + }); + + test("nextVersion does not allow going back", () { + testAllowedVersion("0.12.0", "0.11.0", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.0+1", allowed: false); + testAllowedVersion("0.12.0+1", "0.12.0", allowed: false); + }); + + test("nextVersion allows minor version", () { + testAllowedVersion("0.12.0", "0.12.1", + nextVersionType: NextVersionType.MINOR); + testAllowedVersion("0.12.0+4", "0.12.1", + nextVersionType: NextVersionType.MINOR); + }); + + test("nextVersion does not allow jumping minor", () { + testAllowedVersion("0.12.0", "0.12.2", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.3", allowed: false); + }); + }); + + group("Releasing 1.0", () { + test("nextVersion allows releasing 1.0", () { + testAllowedVersion("0.12.0", "1.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion("0.12.0+4", "1.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test("nextVersion does not allow jumping major", () { + testAllowedVersion("0.12.0", "2.0.0", allowed: false); + testAllowedVersion("0.12.0+4", "2.0.0", allowed: false); + }); + + test("nextVersion does not allow un-releasing", () { + testAllowedVersion("1.0.0", "0.12.0+4", allowed: false); + testAllowedVersion("1.0.0", "0.12.0", allowed: false); + }); + }); + + group("Post 1.0", () { + test("nextVersion allows patch jumps", () { + testAllowedVersion("1.0.1", "1.0.2", + nextVersionType: NextVersionType.PATCH); + testAllowedVersion("1.0.0", "1.0.1", + nextVersionType: NextVersionType.PATCH); + }); + + test("nextVersion does not allow build jumps", () { + testAllowedVersion("1.0.1", "1.0.1+1", allowed: false); + testAllowedVersion("1.0.0+5", "1.0.0+6", allowed: false); + }); + + test("nextVersion does not allow skipping patches", () { + testAllowedVersion("1.0.1", "1.0.3", allowed: false); + testAllowedVersion("1.0.0", "1.0.6", allowed: false); + }); + + test("nextVersion allows minor version jumps", () { + testAllowedVersion("1.0.1", "1.1.0", + nextVersionType: NextVersionType.MINOR); + testAllowedVersion("1.0.0", "1.1.0", + nextVersionType: NextVersionType.MINOR); + }); + + test("nextVersion does not allow skipping minor versions", () { + testAllowedVersion("1.0.1", "1.2.0", allowed: false); + testAllowedVersion("1.1.0", "1.3.0", allowed: false); + }); + + test("nextVersion allows breaking changes", () { + testAllowedVersion("1.0.1", "2.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion("1.0.0", "2.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test("nextVersion allows null safety pre prelease", () { + testAllowedVersion("1.0.1", "2.0.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0", "2.0.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0-nullsafety", "1.0.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0-nullsafety.1", "1.0.0-nullsafety.2", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0", "0.2.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0-nullsafety", "0.1.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0-nullsafety.1", "0.1.0-nullsafety.2", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0", "1.1.0-nullsafety", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.1.0-nullsafety", "1.1.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0", "0.1.1-nullsafety", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.1-nullsafety", "0.1.1-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + }); + + test("nextVersion does not allow skipping major versions", () { + testAllowedVersion("1.0.1", "3.0.0", allowed: false); + testAllowedVersion("1.1.0", "2.3.0", allowed: false); + }); + }); +} diff --git a/packages/flutter_plugin_tools/test/xctest_command_test.dart b/packages/flutter_plugin_tools/test/xctest_command_test.dart new file mode 100644 index 000000000000..007c2e12188c --- /dev/null +++ b/packages/flutter_plugin_tools/test/xctest_command_test.dart @@ -0,0 +1,358 @@ +// 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 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/xctest_command.dart'; +import 'package:test/test.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +final _kDeviceListMap = { + "runtimes": [ + { + "bundlePath": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime", + "buildversion": "17A577", + "runtimeRoot": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-0", + "version": "13.0", + "isAvailable": true, + "name": "iOS 13.0" + }, + { + "bundlePath": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime", + "buildversion": "17L255", + "runtimeRoot": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-4", + "version": "13.4", + "isAvailable": true, + "name": "iOS 13.4" + }, + { + "bundlePath": + "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime", + "buildversion": "17T531", + "runtimeRoot": + "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.watchOS-6-2", + "version": "6.2.1", + "isAvailable": true, + "name": "watchOS 6.2" + } + ], + "devices": { + "com.apple.CoreSimulator.SimRuntime.iOS-13-4": [ + { + "dataPath": + "/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data", + "logPath": + "/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774", + "udid": "2706BBEB-1E01-403E-A8E9-70E8E5A24774", + "isAvailable": true, + "deviceTypeIdentifier": + "com.apple.CoreSimulator.SimDeviceType.iPhone-8", + "state": "Shutdown", + "name": "iPhone 8" + }, + { + "dataPath": + "/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data", + "logPath": + "/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A", + "udid": "1E76A0FD-38AC-4537-A989-EA639D7D012A", + "isAvailable": true, + "deviceTypeIdentifier": + "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus", + "state": "Shutdown", + "name": "iPhone 8 Plus" + } + ] + } +}; + +void main() { + const String _kDestination = '--ios-destination'; + const String _kTarget = '--target'; + const String _kSkip = '--skip'; + + group('test xctest_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final XCTestCommand command = XCTestCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('xctest_command', 'Test for xctest_command'); + runner.addCommand(command); + cleanupPackages(); + }); + + test('Not specifying --target throws', () async { + await expectLater( + () => runner.run(['xctest', _kDestination, 'a_destination']), + throwsA(const TypeMatcher())); + }); + + test('skip if ios is not supported', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + expect(output, contains('iOS is not supported by this plugin.')); + expect(processRunner.recordedCalls, orderedEquals([])); + + cleanupPackages(); + }); + + test('running with correct scheme and destination, did not find scheme', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = '{"project":{"targets":["bar_scheme"]}}'; + + await expectLater(() async { + final List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + expect(output, + contains('foo_scheme not configured for plugin, test failed.')); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('xcrun', ['simctl', 'list', '--json'], null), + ProcessCall( + 'xcodebuild', + [ + '-project', + 'ios/Runner.xcodeproj', + '-list', + '-json' + ], + pluginExampleDirectory.path), + ])); + }, throwsA(const TypeMatcher())); + cleanupPackages(); + }); + + test('running with correct scheme and destination, found scheme', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + + expect(output, contains('Successfully ran xctest for plugin')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'foo_destination', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory.path), + ])); + + cleanupPackages(); + }); + + test('running with correct scheme and destination, skip 1 plugin', + () async { + createFakePlugin('plugin1', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + createFakePlugin('plugin2', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory1 = + mockPackagesDir.childDirectory('plugin1').childDirectory('example'); + createFakePubspec(pluginExampleDirectory1, isFlutter: true); + final Directory pluginExampleDirectory2 = + mockPackagesDir.childDirectory('plugin2').childDirectory('example'); + createFakePubspec(pluginExampleDirectory2, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination', + _kSkip, + 'plugin1' + ]); + + expect(output, contains('plugin1 was skipped with the --skip flag.')); + expect(output, contains('Successfully ran xctest for plugin2')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory2.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'foo_destination', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory2.path), + ])); + + cleanupPackages(); + }); + + test('Not specifying --ios-destination assigns an available simulator', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final Map schemeCommandResult = { + "project": { + "targets": ["bar_scheme", "foo_scheme"] + } + }; + // For simplicity of the test, we combine all the mock results into a single mock result, each internal command + // will get this result and they should still be able to parse them correctly. + processRunner.resultStdout = + jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); + await runner.run([ + 'xctest', + _kTarget, + 'foo_scheme', + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('xcrun', ['simctl', 'list', '--json'], null), + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory.path), + ])); + + cleanupPackages(); + }); + }); +} diff --git a/packages/fluttter_plugin_tools/CHANGELOG.md b/packages/fluttter_plugin_tools/CHANGELOG.md new file mode 100644 index 000000000000..25dce424eee4 --- /dev/null +++ b/packages/fluttter_plugin_tools/CHANGELOG.md @@ -0,0 +1,288 @@ +## v.0.0.45+1 + +- Don't call `flutter format` if there are no Dart files to format. + +## v.0.0.45 + +- Add exclude flag to exclude any plugin from further processing. + +## v.0.0.44+7 + +- `all-plugins-app` doesn't override the AGP version. + +## v.0.0.44+6 + +- Fix code formatting. + +## v.0.0.44+5 + +- Remove `-v` flag on drive-examples. + +## v.0.0.44+4 + +- Fix bug where directory isn't passed + +## v.0.0.44+3 + +- More verbose logging + +## v.0.0.44+2 + +- Remove pre-alpha Windows workaround to create examples on the fly. + +## v.0.0.44+1 + +- Print packages that passed tests in `xctest` command. +- Remove printing the whole list of simulators. + +## v.0.0.44 + +- Add 'xctest' command to run xctests. + +## v.0.0.43 + +- Allow minor `*-nullsafety` pre release packages. + +## v.0.0.42+1 + +- Fix test command when `--enable-experiment` is called. + +## v.0.0.42 + +- Allow `*-nullsafety` pre release packages. + +## v.0.0.41 + +- Support `--enable-experiment` flag in subcommands `test`, `build-examples`, `drive-examples`, +and `firebase-test-lab`. + +## v.0.0.40 + +- Support `integration_test/` directory for `drive-examples` command + +## v.0.0.39 + +- Support `integration_test/` directory for `package:integration_test` + +## v.0.0.38 + +- Add C++ and ObjC++ to clang-format. + +## v.0.0.37+2 + +- Make `http` and `http_multi_server` dependency version constraint more flexible. + +## v.0.0.37+1 + +- All_plugin test puts the plugin dependencies into dependency_overrides. + +## v.0.0.37 + +- Only builds mobile example apps when necessary. + +## v.0.0.36+3 + +- Add support for Linux plugins. + +## v.0.0.36+2 + +- Default to showing podspec lint warnings + +## v.0.0.36+1 + +- Serialize linting podspecs. + +## v.0.0.36 + +- Remove retry on Firebase Test Lab's call to gcloud set. +- Remove quiet flag from Firebase Test Lab's gcloud set command. +- Allow Firebase Test Lab command to continue past gcloud set network failures. + This is a mitigation for the network service sometimes not responding, + but it isn't actually necessary to have a network connection for this command. + +## v.0.0.35+1 + +- Minor cleanup to the analyze test. + +## v.0.0.35 + +- Firebase Test Lab command generates a configurable unique path suffix for results. + +## v.0.0.34 + +- Firebase Test Lab command now only tries to configure the project once +- Firebase Test Lab command now retries project configuration up to five times. + +## v.0.0.33+1 + +- Fixes formatting issues that got past our CI due to + https://github.com/flutter/flutter/issues/51585. +- Changes the default package name for testing method `createFakePubspec` back + its previous behavior. + +## v.0.0.33 + +- Version check command now fails on breaking changes to platform interfaces. +- Updated version check test to be more flexible. + +## v.0.0.32+7 + +- Ensure that Firebase Test Lab tests have a unique storage bucket for each test run. + +## v.0.0.32+6 + +- Ensure that Firebase Test Lab tests have a unique storage bucket for each package. + +## v.0.0.32+5 + +- Remove --fail-fast and --silent from lint podspec command. + +## v.0.0.32+4 + +- Update `publish-plugin` to use `flutter pub publish` instead of just `pub + publish`. Enforces a `pub publish` command that matches the Dart SDK in the + user's Flutter install. + +## v.0.0.32+3 + +- Update Firebase Testlab deprecated test device. (Pixel 3 API 28 -> Pixel 4 API 29). + +## v.0.0.32+2 + +- Runs pub get before building macos to avoid failures. + +## v.0.0.32+1 + +- Default macOS example builds to false. Previously they were running whenever + CI was itself running on macOS. + +## v.0.0.32 + +- `analyze` now asserts that the global `analysis_options.yaml` is the only one + by default. Individual directories can be excluded from this check with the + new `--custom-analysis` flag. + +## v.0.0.31+1 + +- Add --skip and --no-analyze flags to podspec command. + +## v.0.0.31 + +- Add support for macos on `DriveExamplesCommand` and `BuildExamplesCommand`. + +## v.0.0.30 + +- Adopt pedantic analysis options, fix firebase_test_lab_test. + +## v.0.0.29 + +- Add a command to run pod lib lint on podspec files. + +## v.0.0.28 + +- Increase Firebase test lab timeouts to 5 minutes. + +## v.0.0.27 + +- Run tests with `--platform=chrome` for web plugins. + +## v.0.0.26 + +- Add a command for publishing plugins to pub. + +## v.0.0.25 + +- Update `DriveExamplesCommand` to use `ProcessRunner`. +- Make `DriveExamplesCommand` rely on `ProcessRunner` to determine if the test fails or not. +- Add simple tests for `DriveExamplesCommand`. + +## v.0.0.24 + +- Gracefully handle pubspec.yaml files for new plugins. +- Additional unit testing. + +## v.0.0.23 + +- Add a test case for transitive dependency solving in the + `create_all_plugins_app` command. + +## v.0.0.22 + +- Updated firebase-test-lab command with updated conventions for test locations. +- Updated firebase-test-lab to add an optional "device" argument. +- Updated version-check command to always compare refs instead of using the working copy. +- Added unit tests for the firebase-test-lab and version-check commands. +- Add ProcessRunner to mock running processes for testing. + +## v.0.0.21 + +- Support the `--plugins` argument for federated plugins. + +## v.0.0.20 + +- Support for finding federated plugins, where one directory contains + multiple packages for different platform implementations. + +## v.0.0.19+3 + +- Use `package:file` for file I/O. + +## v.0.0.19+2 + +- Use java as language when calling `flutter create`. + +## v.0.0.19+1 + +- Rename command for `CreateAllPluginsAppCommand`. + +## v.0.0.19 + +- Use flutter create to build app testing plugin compilation. + +## v.0.0.18+2 + +- Fix `.travis.yml` file name in `README.md`. + +## v0.0.18+1 + +- Skip version check if it contains `publish_to: none`. + +## v0.0.18 + +- Add option to exclude packages from generated pubspec command. + +## v0.0.17+4 + +- Avoid trying to version-check pubspecs that are missing a version. + +## v0.0.17+3 + +- version-check accounts for [pre-1.0 patch versions](https://github.com/flutter/flutter/issues/35412). + +## v0.0.17+2 + +- Fix exception handling for version checker + +## v0.0.17+1 + +- Fix bug where we used a flag instead of an option + +## v0.0.17 + +- Add a command for checking the version number + +## v0.0.16 + +- Add a command for generating `pubspec.yaml` for All Plugins app. + +## v0.0.15 + +- Add a command for running driver tests of plugin examples. + +## v0.0.14 + +- Check for dependencies->flutter instead of top level flutter node. + +## v0.0.13 + +- Differentiate between Flutter and non-Flutter (but potentially Flutter consumed) Dart packages. diff --git a/packages/fluttter_plugin_tools/LICENSE b/packages/fluttter_plugin_tools/LICENSE new file mode 100644 index 000000000000..5f015bc7b321 --- /dev/null +++ b/packages/fluttter_plugin_tools/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2017, Flutter +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/fluttter_plugin_tools/README.md b/packages/fluttter_plugin_tools/README.md new file mode 100644 index 000000000000..efdc668e81a0 --- /dev/null +++ b/packages/fluttter_plugin_tools/README.md @@ -0,0 +1,45 @@ +# Flutter Plugin Tools + +[![Build Status](https://travis-ci.org/flutter/plugin_tools.svg?branch=master)](https://travis-ci.org/flutter/plugin_tools) +[![pub package](https://img.shields.io/pub/v/flutter_plugin_tools.svg)](https://pub.dartlang.org/packages/flutter_plugin_tools) + + +Flutter Plugin Tools implements a CLI with various productivity tools for hosting multiple Flutter plugins in one github +repository. It is mainly used by the [flutter/plugins](https://github.com/flutter/plugins) and +[flutter/flutterfire](https://github.com/flutter/flutterfire) repositories. It was mainly written to facilitate +testing on Travis for these repositories (see [.travis.yml](https://github.com/flutter/plugins/blob/master/.travis.yml)). + +As an example, Flutter Plugin Tools allows you to: + +* Build all plugin example apps with one command +* Run the tests of all plugins with one command +* Format all Dart, Java, Objective-C, and C++ code in the repository +* Define shards of the above tasks + +## Installation + +In order to use the tools you need to enable them once by running the following command: + +```shell +$ pub global activate flutter_plugin_tools +``` + +## Requirements + +To use all features of `flutter_plugin_tools` you'll need the following commands in your `PATH`: +* `flutter` +* `git` +* `pub` (recommended: version from `/bin/cache/dart-sdk/bin`) +* `clang-format` version 5 (alternatively, you can provide the path via `--clang-format=`) +* [`pod`](https://guides.cocoapods.org/using/getting-started.html#installation) (macOS only) + +## Usage + +```shell +$ pub global run flutter_plugin_tools +$ pub global run flutter_plugin_tools --shardIndex 0 --shardCount 3 +``` + +Run commands from the `flutter/plugins` directory. Replace `` with `help` to print a list of available commands. +The sharded example above divides the plugins into three shards +and executes the tool on the first shard (index 0). diff --git a/packages/fluttter_plugin_tools/analysis_options.yaml b/packages/fluttter_plugin_tools/analysis_options.yaml new file mode 100644 index 000000000000..84a5e26f95de --- /dev/null +++ b/packages/fluttter_plugin_tools/analysis_options.yaml @@ -0,0 +1 @@ +include: package:pedantic/analysis_options.1.8.0.yaml diff --git a/packages/fluttter_plugin_tools/bin/flutter_plugin_tools.dart b/packages/fluttter_plugin_tools/bin/flutter_plugin_tools.dart new file mode 100644 index 000000000000..43edcbfe2c68 --- /dev/null +++ b/packages/fluttter_plugin_tools/bin/flutter_plugin_tools.dart @@ -0,0 +1,5 @@ +// 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. + +export 'package:flutter_plugin_tools/src/main.dart'; diff --git a/packages/fluttter_plugin_tools/lib/src/analyze_command.dart b/packages/fluttter_plugin_tools/lib/src/analyze_command.dart new file mode 100644 index 000000000000..8cd57fa0b338 --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/analyze_command.dart @@ -0,0 +1,94 @@ +// 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:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class AnalyzeCommand extends PluginCommand { + AnalyzeCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addMultiOption(_customAnalysisFlag, + help: + 'Directories (comma seperated) that are allowed to have their own analysis options.', + defaultsTo: []); + } + + static const String _customAnalysisFlag = 'custom-analysis'; + + @override + final String name = 'analyze'; + + @override + final String description = 'Analyzes all packages using package:tuneup.\n\n' + 'This command requires "pub" and "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + + print('Verifying analysis settings...'); + final List files = packagesDir.listSync(recursive: true); + for (final FileSystemEntity file in files) { + if (file.basename != 'analysis_options.yaml' && + file.basename != '.analysis_options') { + continue; + } + + final bool whitelisted = argResults[_customAnalysisFlag].any( + (String directory) => + p.isWithin(p.join(packagesDir.path, directory), file.path)); + if (whitelisted) { + continue; + } + + print('Found an extra analysis_options.yaml in ${file.absolute.path}.'); + print( + 'If this was deliberate, pass the package to the analyze command with the --$_customAnalysisFlag flag and try again.'); + throw ToolExit(1); + } + + print('Activating tuneup package...'); + await processRunner.runAndStream( + 'pub', ['global', 'activate', 'tuneup'], + workingDir: packagesDir, exitOnError: true); + + await for (Directory package in getPackages()) { + if (isFlutterPackage(package, fileSystem)) { + await processRunner.runAndStream('flutter', ['packages', 'get'], + workingDir: package, exitOnError: true); + } else { + await processRunner.runAndStream('pub', ['get'], + workingDir: package, exitOnError: true); + } + } + + final List failingPackages = []; + await for (Directory package in getPlugins()) { + final int exitCode = await processRunner.runAndStream( + 'pub', ['global', 'run', 'tuneup', 'check'], + workingDir: package); + if (exitCode != 0) { + failingPackages.add(p.basename(package.path)); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print('The following packages have analyzer errors (see above):'); + failingPackages.forEach((String package) { + print(' * $package'); + }); + throw ToolExit(1); + } + + print('No analyzer errors found!'); + } +} diff --git a/packages/fluttter_plugin_tools/lib/src/build_examples_command.dart b/packages/fluttter_plugin_tools/lib/src/build_examples_command.dart new file mode 100644 index 000000000000..1493b3cce05d --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/build_examples_command.dart @@ -0,0 +1,191 @@ +// 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 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'common.dart'; + +class BuildExamplesCommand extends PluginCommand { + BuildExamplesCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag(kLinux, defaultsTo: false); + argParser.addFlag(kMacos, defaultsTo: false); + argParser.addFlag(kWindows, defaultsTo: false); + argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS); + argParser.addFlag(kApk); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Enables the given Dart SDK experiments.', + ); + } + + @override + final String name = 'build-examples'; + + @override + final String description = + 'Builds all example apps (IPA for iOS and APK for Android).\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + if (!argResults[kIpa] && + !argResults[kApk] && + !argResults[kLinux] && + !argResults[kMacos] && + !argResults[kWindows]) { + print( + 'None of --linux, --macos, --windows, --apk nor --ipa were specified, ' + 'so not building anything.'); + return; + } + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + + final String enableExperiment = argResults[kEnableExperiment]; + + checkSharding(); + final List failingPackages = []; + await for (Directory plugin in getPlugins()) { + for (Directory example in getExamplesForPlugin(plugin)) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + + if (argResults[kLinux]) { + print('\nBUILDING Linux for $packageName'); + if (isLinuxPlugin(plugin, fileSystem)) { + int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kLinux, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (linux)'); + } + } else { + print('Linux is not supported by this plugin'); + } + } + + if (argResults[kMacos]) { + print('\nBUILDING macOS for $packageName'); + if (isMacOsPlugin(plugin, fileSystem)) { + // TODO(https://github.com/flutter/flutter/issues/46236): + // Builing macos without running flutter pub get first results + // in an error. + int exitCode = await processRunner.runAndStream( + flutterCommand, ['pub', 'get'], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (macos)'); + } else { + exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kMacos, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (macos)'); + } + } + } else { + print('macOS is not supported by this plugin'); + } + } + + if (argResults[kWindows]) { + print('\nBUILDING Windows for $packageName'); + if (isWindowsPlugin(plugin, fileSystem)) { + int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kWindows, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (windows)'); + } + } else { + print('Windows is not supported by this plugin'); + } + } + + if (argResults[kIpa]) { + print('\nBUILDING IPA for $packageName'); + if (isIosPlugin(plugin, fileSystem)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + '--verbose', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example, + exitOnError: true); + if (exitCode != 0) { + failingPackages.add('$packageName (ipa)'); + } + } else { + print('iOS is not supported by this plugin'); + } + } + print('end ios build'); + + if (argResults[kApk]) { + print('\nBUILDING APK for $packageName'); + if (isAndroidPlugin(plugin, fileSystem)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + 'apk', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (apk)'); + } + } else { + print('Android is not supported by this plugin'); + } + } + } + } + print('\n\n'); + + if (failingPackages.isNotEmpty) { + print('The following build are failing (see above for details):'); + for (String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + + print('All builds successful!'); + } +} diff --git a/packages/fluttter_plugin_tools/lib/src/common.dart b/packages/fluttter_plugin_tools/lib/src/common.dart new file mode 100644 index 000000000000..78b91ee8a75b --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/common.dart @@ -0,0 +1,466 @@ +// 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 'dart:io' as io; +import 'dart:math'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; + +typedef void Print(Object object); + +/// Key for windows platform. +const String kWindows = 'windows'; + +/// Key for macos platform. +const String kMacos = 'macos'; + +/// Key for linux platform. +const String kLinux = 'linux'; + +/// Key for IPA (iOS) platform. +const String kIos = 'ios'; + +/// Key for APK (Android) platform. +const String kAndroid = 'android'; + +/// Key for Web platform. +const String kWeb = 'web'; + +/// Key for IPA. +const String kIpa = 'ipa'; + +/// Key for APK. +const String kApk = 'apk'; + +/// Key for enable experiment. +const String kEnableExperiment = 'enable-experiment'; + +/// Returns whether the given directory contains a Flutter package. +bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) { + if (entity == null || entity is! Directory) { + return false; + } + + try { + final File pubspecFile = + fileSystem.file(p.join(entity.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final YamlMap dependencies = pubspecYaml['dependencies']; + if (dependencies == null) { + return false; + } + return dependencies.containsKey('flutter'); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Returns whether the given directory contains a Flutter [platform] plugin. +/// +/// It checks this by looking for the following pattern in the pubspec: +/// +/// flutter: +/// plugin: +/// platforms: +/// [platform]: +bool pluginSupportsPlatform( + String platform, FileSystemEntity entity, FileSystem fileSystem) { + assert(platform == kIos || + platform == kAndroid || + platform == kWeb || + platform == kMacos || + platform == kWindows || + platform == kLinux); + if (entity == null || entity is! Directory) { + return false; + } + + try { + final File pubspecFile = + fileSystem.file(p.join(entity.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final YamlMap flutterSection = pubspecYaml['flutter']; + if (flutterSection == null) { + return false; + } + final YamlMap pluginSection = flutterSection['plugin']; + if (pluginSection == null) { + return false; + } + final YamlMap platforms = pluginSection['platforms']; + if (platforms == null) { + // Legacy plugin specs are assumed to support iOS and Android. + if (!pluginSection.containsKey('platforms')) { + return platform == kIos || platform == kAndroid; + } + return false; + } + return platforms.containsKey(platform); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Returns whether the given directory contains a Flutter Android plugin. +bool isAndroidPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kAndroid, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter iOS plugin. +bool isIosPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kIos, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter web plugin. +bool isWebPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kWeb, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter Windows plugin. +bool isWindowsPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kWindows, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter macOS plugin. +bool isMacOsPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kMacos, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter linux plugin. +bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kLinux, entity, fileSystem); +} + +/// Error thrown when a command needs to exit with a non-zero exit code. +class ToolExit extends Error { + ToolExit(this.exitCode); + + final int exitCode; +} + +abstract class PluginCommand extends Command { + PluginCommand( + this.packagesDir, + this.fileSystem, { + this.processRunner = const ProcessRunner(), + }) { + argParser.addMultiOption( + _pluginsArg, + splitCommas: true, + help: + 'Specifies which plugins the command should run on (before sharding).', + valueHelp: 'plugin1,plugin2,...', + ); + argParser.addOption( + _shardIndexArg, + help: 'Specifies the zero-based index of the shard to ' + 'which the command applies.', + valueHelp: 'i', + defaultsTo: '0', + ); + argParser.addOption( + _shardCountArg, + help: 'Specifies the number of shards into which plugins are divided.', + valueHelp: 'n', + defaultsTo: '1', + ); + argParser.addMultiOption( + _excludeArg, + abbr: 'e', + help: 'Exclude packages from this command.', + defaultsTo: [], + ); + } + + static const String _pluginsArg = 'plugins'; + static const String _shardIndexArg = 'shardIndex'; + static const String _shardCountArg = 'shardCount'; + static const String _excludeArg = 'exclude'; + + /// The directory containing the plugin packages. + final Directory packagesDir; + + /// The file system. + /// + /// This can be overridden for testing. + final FileSystem fileSystem; + + /// The process runner. + /// + /// This can be overridden for testing. + final ProcessRunner processRunner; + + int _shardIndex; + int _shardCount; + + int get shardIndex { + if (_shardIndex == null) { + checkSharding(); + } + return _shardIndex; + } + + int get shardCount { + if (_shardCount == null) { + checkSharding(); + } + return _shardCount; + } + + void checkSharding() { + final int shardIndex = int.tryParse(argResults[_shardIndexArg]); + final int shardCount = int.tryParse(argResults[_shardCountArg]); + if (shardIndex == null) { + usageException('$_shardIndexArg must be an integer'); + } + if (shardCount == null) { + usageException('$_shardCountArg must be an integer'); + } + if (shardCount < 1) { + usageException('$_shardCountArg must be positive'); + } + if (shardIndex < 0 || shardCount <= shardIndex) { + usageException( + '$_shardIndexArg must be in the half-open range [0..$shardCount['); + } + _shardIndex = shardIndex; + _shardCount = shardCount; + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution. + Stream getPlugins() async* { + // To avoid assuming consistency of `Directory.list` across command + // invocations, we collect and sort the plugin folders before sharding. + // This is considered an implementation detail which is why the API still + // uses streams. + final List allPlugins = await _getAllPlugins().toList(); + allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path)); + // Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2. + // Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3. + // Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0. + final int shardSize = allPlugins.length ~/ shardCount + + (allPlugins.length % shardCount == 0 ? 0 : 1); + final int start = min(shardIndex * shardSize, allPlugins.length); + final int end = min(start + shardSize, allPlugins.length); + + for (Directory plugin in allPlugins.sublist(start, end)) { + yield plugin; + } + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution, assuming there is only one shard. + /// + /// Plugin packages can exist in one of two places relative to the packages + /// directory. + /// + /// 1. As a Dart package in a directory which is a direct child of the + /// packages directory. This is a plugin where all of the implementations + /// exist in a single Dart package. + /// 2. Several plugin packages may live in a directory which is a direct + /// child of the packages directory. This directory groups several Dart + /// packages which implement a single plugin. This directory contains a + /// "client library" package, which declares the API for the plugin, as + /// well as one or more platform-specific implementations. + Stream _getAllPlugins() async* { + final Set plugins = Set.from(argResults[_pluginsArg]); + final Set excludedPlugins = + Set.from(argResults[_excludeArg]); + + await for (FileSystemEntity entity + in packagesDir.list(followLinks: false)) { + // A top-level Dart package is a plugin package. + if (_isDartPackage(entity)) { + if (!excludedPlugins.contains(entity.basename) && + (plugins.isEmpty || plugins.contains(p.basename(entity.path)))) { + yield entity; + } + } else if (entity is Directory) { + // Look for Dart packages under this top-level directory. + await for (FileSystemEntity subdir in entity.list(followLinks: false)) { + if (_isDartPackage(subdir)) { + // If --plugin=my_plugin is passed, then match all federated + // plugins under 'my_plugin'. Also match if the exact plugin is + // passed. + final String relativePath = + p.relative(subdir.path, from: packagesDir.path); + final String basenamePath = p.basename(entity.path); + if (!excludedPlugins.contains(basenamePath) && + !excludedPlugins.contains(relativePath) && + (plugins.isEmpty || + plugins.contains(relativePath) || + plugins.contains(basenamePath))) { + yield subdir; + } + } + } + } + } + } + + /// Returns the example Dart package folders of the plugins involved in this + /// command execution. + Stream getExamples() => + getPlugins().expand(getExamplesForPlugin); + + /// Returns all Dart package folders (typically, plugin + example) of the + /// plugins involved in this command execution. + Stream getPackages() async* { + await for (Directory plugin in getPlugins()) { + yield plugin; + yield* plugin + .list(recursive: true, followLinks: false) + .where(_isDartPackage) + .cast(); + } + } + + /// Returns the files contained, recursively, within the plugins + /// involved in this command execution. + Stream getFiles() { + return getPlugins().asyncExpand((Directory folder) => folder + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => entity is File) + .cast()); + } + + /// Returns whether the specified entity is a directory containing a + /// `pubspec.yaml` file. + bool _isDartPackage(FileSystemEntity entity) { + return entity is Directory && + fileSystem.file(p.join(entity.path, 'pubspec.yaml')).existsSync(); + } + + /// Returns the example Dart packages contained in the specified plugin, or + /// an empty List, if the plugin has no examples. + Iterable getExamplesForPlugin(Directory plugin) { + final Directory exampleFolder = + fileSystem.directory(p.join(plugin.path, 'example')); + if (!exampleFolder.existsSync()) { + return []; + } + if (isFlutterPackage(exampleFolder, fileSystem)) { + return [exampleFolder]; + } + // Only look at the subdirectories of the example directory if the example + // directory itself is not a Dart package, and only look one level below the + // example directory for other dart packages. + return exampleFolder + .listSync() + .where( + (FileSystemEntity entity) => isFlutterPackage(entity, fileSystem)) + .cast(); + } +} + +/// A class used to run processes. +/// +/// We use this instead of directly running the process so it can be overridden +/// in tests. +class ProcessRunner { + const ProcessRunner(); + + /// Run the [executable] with [args] and stream output to stderr and stdout. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// + /// Returns the exit code of the [executable]. + Future runAndStream( + String executable, + List args, { + Directory workingDir, + bool exitOnError = false, + }) async { + print( + 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDir?.path); + await io.stdout.addStream(process.stdout); + await io.stderr.addStream(process.stderr); + if (exitOnError && await process.exitCode != 0) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error See above for details.'); + throw ToolExit(await process.exitCode); + } + return process.exitCode; + } + + /// Run the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// + /// Returns the [io.ProcessResult] of the [executable]. + Future run(String executable, List args, + {Directory workingDir, + bool exitOnError = false, + stdoutEncoding = io.systemEncoding, + stderrEncoding = io.systemEncoding}) async { + return io.Process.run(executable, args, + workingDirectory: workingDir?.path, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding); + } + + /// Starts the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// Returns the started [io.Process]. + Future start(String executable, List args, + {Directory workingDirectory}) async { + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDirectory?.path); + return process; + } + + /// Run the [executable] with [args], throwing an error on non-zero exit code. + /// + /// Unlike [runAndStream], this does not stream the process output to stdout. + /// It also unconditionally throws an error on a non-zero exit code. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// Returns the [io.ProcessResult] of running the [executable]. + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + final io.ProcessResult result = await io.Process.run(executable, args, + workingDirectory: workingDir?.path); + if (result.exitCode != 0) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error Stderr:\n${result.stdout}'); + throw ToolExit(result.exitCode); + } + return result; + } + + String _getErrorString(String executable, List args, + {Directory workingDir}) { + final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; + return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; + } +} diff --git a/packages/fluttter_plugin_tools/lib/src/create_all_plugins_app_command.dart b/packages/fluttter_plugin_tools/lib/src/create_all_plugins_app_command.dart new file mode 100644 index 000000000000..0f1431c5aee0 --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/create_all_plugins_app_command.dart @@ -0,0 +1,200 @@ +// 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:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common.dart'; + +// TODO(cyanglaz): Add tests for this command. +// https://github.com/flutter/flutter/issues/61049 +class CreateAllPluginsAppCommand extends PluginCommand { + CreateAllPluginsAppCommand(Directory packagesDir, FileSystem fileSystem) + : super(packagesDir, fileSystem); + + @override + String get description => + 'Generate Flutter app that includes all plugins in packages.'; + + @override + String get name => 'all-plugins-app'; + + @override + Future run() async { + final int exitCode = await _createPlugin(); + if (exitCode != 0) { + throw ToolExit(exitCode); + } + + await Future.wait(>[ + _genPubspecWithAllPlugins(), + _updateAppGradle(), + _updateManifest(), + ]); + } + + Future _createPlugin() async { + final io.ProcessResult result = io.Process.runSync( + 'flutter', + [ + 'create', + '--template=app', + '--project-name=all_plugins', + '--android-language=java', + './all_plugins', + ], + ); + + print(result.stdout); + print(result.stderr); + return result.exitCode; + } + + Future _updateAppGradle() async { + final File gradleFile = fileSystem.file(p.join( + 'all_plugins', + 'android', + 'app', + 'build.gradle', + )); + if (!gradleFile.existsSync()) { + throw ToolExit(64); + } + + final StringBuffer newGradle = StringBuffer(); + for (String line in gradleFile.readAsLinesSync()) { + newGradle.writeln(line); + if (line.contains('defaultConfig {')) { + newGradle.writeln(' multiDexEnabled true'); + } else if (line.contains('dependencies {')) { + newGradle.writeln( + ' implementation \'com.google.guava:guava:27.0.1-android\'\n', + ); + // Tests for https://github.com/flutter/flutter/issues/43383 + newGradle.writeln( + " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", + ); + } + } + gradleFile.writeAsStringSync(newGradle.toString()); + } + + Future _updateManifest() async { + final File manifestFile = fileSystem.file(p.join( + 'all_plugins', + 'android', + 'app', + 'src', + 'main', + 'AndroidManifest.xml', + )); + if (!manifestFile.existsSync()) { + throw ToolExit(64); + } + + final StringBuffer newManifest = StringBuffer(); + for (String line in manifestFile.readAsLinesSync()) { + if (line.contains('package="com.example.all_plugins"')) { + newManifest + ..writeln('package="com.example.all_plugins"') + ..writeln('xmlns:tools="http://schemas.android.com/tools">') + ..writeln() + ..writeln( + '', + ); + } else { + newManifest.writeln(line); + } + } + manifestFile.writeAsStringSync(newManifest.toString()); + } + + Future _genPubspecWithAllPlugins() async { + final Map pluginDeps = + await _getValidPathDependencies(); + final Pubspec pubspec = Pubspec( + 'all_plugins', + description: 'Flutter app containing all 1st party plugins.', + version: Version.parse('1.0.0+1'), + environment: { + 'sdk': VersionConstraint.compatibleWith( + Version.parse('2.0.0'), + ), + }, + dependencies: { + 'flutter': SdkDependency('flutter'), + }..addAll(pluginDeps), + devDependencies: { + 'flutter_test': SdkDependency('flutter'), + }, + dependencyOverrides: pluginDeps, + ); + final File pubspecFile = + fileSystem.file(p.join('all_plugins', 'pubspec.yaml')); + pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); + } + + Future> _getValidPathDependencies() async { + final Map pathDependencies = + {}; + + await for (Directory package in getPlugins()) { + final String pluginName = package.path.split('/').last; + final File pubspecFile = + fileSystem.file(p.join(package.path, 'pubspec.yaml')); + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + + if (pubspec.publishTo != 'none') { + pathDependencies[pluginName] = PathDependency(package.path); + } + } + return pathDependencies; + } + + String _pubspecToString(Pubspec pubspec) { + return ''' +### Generated file. Do not edit. Run `pub global run flutter_plugin_tools gen-pubspec` to update. +name: ${pubspec.name} +description: ${pubspec.description} + +version: ${pubspec.version} + +environment:${_pubspecMapString(pubspec.environment)} + +dependencies:${_pubspecMapString(pubspec.dependencies)} + +dependency_overrides:${_pubspecMapString(pubspec.dependencyOverrides)} + +dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} +###'''; + } + + String _pubspecMapString(Map values) { + final StringBuffer buffer = StringBuffer(); + + for (MapEntry entry in values.entries) { + buffer.writeln(); + if (entry.value is VersionConstraint) { + buffer.write(' ${entry.key}: ${entry.value}'); + } else if (entry.value is SdkDependency) { + final SdkDependency dep = entry.value; + buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); + } else if (entry.value is PathDependency) { + final PathDependency dep = entry.value; + buffer.write(' ${entry.key}: \n path: ${dep.path}'); + } else { + throw UnimplementedError( + 'Not available for type: ${entry.value.runtimeType}', + ); + } + } + + return buffer.toString(); + } +} diff --git a/packages/fluttter_plugin_tools/lib/src/drive_examples_command.dart b/packages/fluttter_plugin_tools/lib/src/drive_examples_command.dart new file mode 100644 index 000000000000..8b1fa3624fbf --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/drive_examples_command.dart @@ -0,0 +1,210 @@ +// 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:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'common.dart'; + +class DriveExamplesCommand extends PluginCommand { + DriveExamplesCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag(kLinux, + help: 'Runs the Linux implementation of the examples'); + argParser.addFlag(kMacos, + help: 'Runs the macOS implementation of the examples'); + argParser.addFlag(kWindows, + help: 'Runs the Windows implementation of the examples'); + argParser.addFlag(kIos, + help: 'Runs the iOS implementation of the examples'); + argParser.addFlag(kAndroid, + help: 'Runs the Android implementation of the examples'); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: + 'Runs the driver tests in Dart VM with the given experiments enabled.', + ); + } + + @override + final String name = 'drive-examples'; + + @override + final String description = 'Runs driver tests for plugin example apps.\n\n' + 'For each *_test.dart in test_driver/ it drives an application with a ' + 'corresponding name in the test/ or test_driver/ directories.\n\n' + 'For example, test_driver/app_test.dart would match test/app.dart.\n\n' + 'This command requires "flutter" to be in your path.\n\n' + 'If a file with a corresponding name cannot be found, this driver file' + 'will be used to drive the tests that match ' + 'integration_test/*_test.dart.'; + + @override + Future run() async { + checkSharding(); + final List failingTests = []; + final bool isLinux = argResults[kLinux]; + final bool isMacos = argResults[kMacos]; + final bool isWindows = argResults[kWindows]; + await for (Directory plugin in getPlugins()) { + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + for (Directory example in getExamplesForPlugin(plugin)) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + if (!(await pluginSupportedOnCurrentPlatform(plugin, fileSystem))) { + continue; + } + final Directory driverTests = + fileSystem.directory(p.join(example.path, 'test_driver')); + if (!driverTests.existsSync()) { + // No driver tests available for this example + continue; + } + // Look for driver tests ending in _test.dart in test_driver/ + await for (FileSystemEntity test in driverTests.list()) { + final String driverTestName = + p.relative(test.path, from: driverTests.path); + if (!driverTestName.endsWith('_test.dart')) { + continue; + } + // Try to find a matching app to drive without the _test.dart + final String deviceTestName = driverTestName.replaceAll( + RegExp(r'_test.dart$'), + '.dart', + ); + String deviceTestPath = p.join('test', deviceTestName); + if (!fileSystem + .file(p.join(example.path, deviceTestPath)) + .existsSync()) { + // If the app isn't in test/ folder, look in test_driver/ instead. + deviceTestPath = p.join('test_driver', deviceTestName); + } + + final List targetPaths = []; + if (fileSystem + .file(p.join(example.path, deviceTestPath)) + .existsSync()) { + targetPaths.add(deviceTestPath); + } else { + final Directory integrationTests = + fileSystem.directory(p.join(example.path, 'integration_test')); + + if (await integrationTests.exists()) { + await for (FileSystemEntity integration_test + in integrationTests.list()) { + if (!integration_test.basename.endsWith('_test.dart')) { + continue; + } + targetPaths + .add(p.relative(integration_test.path, from: example.path)); + } + } + + if (targetPaths.isEmpty) { + print(''' +Unable to infer a target application for $driverTestName to drive. +Tried searching for the following: +1. test/$deviceTestName +2. test_driver/$deviceTestName +3. test_driver/*_test.dart +'''); + failingTests.add(p.relative(test.path, from: example.path)); + continue; + } + } + + final List driveArgs = ['drive', '-v']; + + final String enableExperiment = argResults[kEnableExperiment]; + if (enableExperiment.isNotEmpty) { + driveArgs.add('--enable-experiment=$enableExperiment'); + } + + if (isLinux && isLinuxPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'linux', + ]); + } + if (isMacos && isMacOsPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'macos', + ]); + } + if (isWindows && isWindowsPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'windows', + ]); + } + + for (final targetPath in targetPaths) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + ...driveArgs, + '--driver', + p.join('test_driver', driverTestName), + '--target', + targetPath, + ], + workingDir: example, + exitOnError: true); + if (exitCode != 0) { + failingTests.add(p.join(packageName, deviceTestPath)); + } + } + } + } + } + print('\n\n'); + + if (failingTests.isNotEmpty) { + print('The following driver tests are failing (see above for details):'); + for (String test in failingTests) { + print(' * $test'); + } + throw ToolExit(1); + } + + print('All driver tests successful!'); + } + + Future pluginSupportedOnCurrentPlatform( + FileSystemEntity plugin, FileSystem fileSystem) async { + final bool isLinux = argResults[kLinux]; + final bool isMacos = argResults[kMacos]; + final bool isWindows = argResults[kWindows]; + final bool isIOS = argResults[kIos]; + final bool isAndroid = argResults[kAndroid]; + if (isLinux) { + return isLinuxPlugin(plugin, fileSystem); + } + if (isMacos) { + return isMacOsPlugin(plugin, fileSystem); + } + if (isWindows) { + return isWindowsPlugin(plugin, fileSystem); + } + if (isIOS) { + return isIosPlugin(plugin, fileSystem); + } + if (isAndroid) { + return (isAndroidPlugin(plugin, fileSystem)); + } + // When we are here, no flags are specified. Only return true if the plugin supports mobile for legacy command support. + // TODO(cyanglaz): Make mobile platforms flags also required like other platforms (breaking change). + // https://github.com/flutter/flutter/issues/58285 + final bool isMobilePlugin = + isIosPlugin(plugin, fileSystem) || isAndroidPlugin(plugin, fileSystem); + return isMobilePlugin; + } +} diff --git a/packages/fluttter_plugin_tools/lib/src/firebase_test_lab_command.dart b/packages/fluttter_plugin_tools/lib/src/firebase_test_lab_command.dart new file mode 100644 index 000000000000..0b4b2a471dbc --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/firebase_test_lab_command.dart @@ -0,0 +1,264 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:uuid/uuid.dart'; + +import 'common.dart'; + +class FirebaseTestLabCommand extends PluginCommand { + FirebaseTestLabCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + Print print = print, + }) : _print = print, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + 'project', + defaultsTo: 'flutter-infra', + help: 'The Firebase project name.', + ); + argParser.addOption('service-key', + defaultsTo: + p.join(io.Platform.environment['HOME'], 'gcloud-service-key.json')); + argParser.addOption('test-run-id', + defaultsTo: Uuid().v4(), + help: + 'Optional string to append to the results path, to avoid conflicts. ' + 'Randomly chosen on each invocation if none is provided. ' + 'The default shown here is just an example.'); + argParser.addMultiOption('device', + splitCommas: false, + defaultsTo: [ + 'model=walleye,version=26', + 'model=flame,version=29' + ], + help: + 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); + argParser.addOption('results-bucket', + defaultsTo: 'gs://flutter_firebase_testlab'); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Enables the given Dart SDK experiments.', + ); + } + + @override + final String name = 'firebase-test-lab'; + + @override + final String description = 'Runs the instrumentation tests of the example ' + 'apps on Firebase Test Lab.\n\n' + 'Runs tests in test_instrumentation folder using the ' + 'instrumentation_test package.'; + + static const String _gradleWrapper = 'gradlew'; + + final Print _print; + + Completer _firebaseProjectConfigured; + + Future _configureFirebaseProject() async { + if (_firebaseProjectConfigured != null) { + return _firebaseProjectConfigured.future; + } else { + _firebaseProjectConfigured = Completer(); + } + await processRunner.runAndExitOnError('gcloud', [ + 'auth', + 'activate-service-account', + '--key-file=${argResults['service-key']}', + ]); + int exitCode = await processRunner.runAndStream('gcloud', [ + 'config', + 'set', + 'project', + argResults['project'], + ]); + if (exitCode == 0) { + _print('\nFirebase project configured.'); + return; + } else { + _print( + '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.'); + } + _firebaseProjectConfigured.complete(null); + } + + @override + Future run() async { + checkSharding(); + final Stream packagesWithTests = getPackages().where( + (Directory d) => + isFlutterPackage(d, fileSystem) && + fileSystem + .directory(p.join( + d.path, 'example', 'android', 'app', 'src', 'androidTest')) + .existsSync()); + + final List failingPackages = []; + final List missingFlutterBuild = []; + int resultsCounter = + 0; // We use a unique GCS bucket for each Firebase Test Lab run + await for (Directory package in packagesWithTests) { + // See https://github.com/flutter/flutter/issues/38983 + + final Directory exampleDirectory = + fileSystem.directory(p.join(package.path, 'example')); + final String packageName = + p.relative(package.path, from: packagesDir.path); + _print('\nRUNNING FIREBASE TEST LAB TESTS for $packageName'); + + final Directory androidDirectory = + fileSystem.directory(p.join(exampleDirectory.path, 'android')); + + final String enableExperiment = argResults[kEnableExperiment]; + final String encodedEnableExperiment = + Uri.encodeComponent('--enable-experiment=$enableExperiment'); + + // Ensures that gradle wrapper exists + if (!fileSystem + .file(p.join(androidDirectory.path, _gradleWrapper)) + .existsSync()) { + final int exitCode = await processRunner.runAndStream( + 'flutter', + [ + 'build', + 'apk', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + continue; + } + + await _configureFirebaseProject(); + + int exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + [ + 'app:assembleAndroidTest', + '-Pverbose=true', + if (enableExperiment.isNotEmpty) + '-Pextra-front-end-options=$encodedEnableExperiment', + if (enableExperiment.isNotEmpty) + '-Pextra-gen-snapshot-options=$encodedEnableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + + // Look for tests recursively in folders that start with 'test' and that + // live in the root or example folders. + bool isTestDir(FileSystemEntity dir) { + return p.basename(dir.path).startsWith('test') || + p.basename(dir.path) == 'integration_test'; + } + + final List testDirs = + package.listSync().where(isTestDir).toList(); + final Directory example = + fileSystem.directory(p.join(package.path, 'example')); + testDirs.addAll(example.listSync().where(isTestDir).toList()); + for (Directory testDir in testDirs) { + bool isE2ETest(FileSystemEntity file) { + return file.path.endsWith('_e2e.dart') || + (file.parent.basename == 'integration_test' && + file.path.endsWith('_test.dart')); + } + + final List testFiles = testDir + .listSync(recursive: true, followLinks: true) + .where(isE2ETest) + .toList(); + for (FileSystemEntity test in testFiles) { + exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + [ + 'app:assembleDebug', + '-Pverbose=true', + '-Ptarget=${test.path}', + if (enableExperiment.isNotEmpty) + '-Pextra-front-end-options=$encodedEnableExperiment', + if (enableExperiment.isNotEmpty) + '-Pextra-gen-snapshot-options=$encodedEnableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + final String buildId = io.Platform.environment['CIRRUS_BUILD_ID']; + final String testRunId = argResults['test-run-id']; + final String resultsDir = + 'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/'; + final List args = [ + 'firebase', + 'test', + 'android', + 'run', + '--type', + 'instrumentation', + '--app', + 'build/app/outputs/apk/debug/app-debug.apk', + '--test', + 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', + '--timeout', + '5m', + '--results-bucket=${argResults['results-bucket']}', + '--results-dir=${resultsDir}', + ]; + for (String device in argResults['device']) { + args.addAll(['--device', device]); + } + exitCode = await processRunner.runAndStream('gcloud', args, + workingDir: exampleDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + } + } + } + + _print('\n\n'); + if (failingPackages.isNotEmpty) { + _print( + 'The instrumentation tests for the following packages are failing (see above for' + 'details):'); + for (String package in failingPackages) { + _print(' * $package'); + } + } + if (missingFlutterBuild.isNotEmpty) { + _print('Run "pub global run flutter_plugin_tools build-examples --apk" on' + 'the following packages before executing tests again:'); + for (String package in missingFlutterBuild) { + _print(' * $package'); + } + } + + if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { + throw ToolExit(1); + } + + _print('All Firebase Test Lab tests successful!'); + } +} diff --git a/packages/fluttter_plugin_tools/lib/src/format_command.dart b/packages/fluttter_plugin_tools/lib/src/format_command.dart new file mode 100644 index 000000000000..ec326b96c1f9 --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/format_command.dart @@ -0,0 +1,147 @@ +// 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 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as p; +import 'package:quiver/iterables.dart'; + +import 'common.dart'; + +const String _googleFormatterUrl = + 'https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'; + +class FormatCommand extends PluginCommand { + FormatCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag('travis', hide: true); + argParser.addOption('clang-format', + defaultsTo: 'clang-format', + help: 'Path to executable of clang-format v5.'); + } + + @override + final String name = 'format'; + + @override + final String description = + 'Formats the code of all packages (Java, Objective-C, C++, and Dart).\n\n' + 'This command requires "git", "flutter" and "clang-format" v5 to be in ' + 'your path.'; + + @override + Future run() async { + checkSharding(); + final String googleFormatterPath = await _getGoogleFormatterPath(); + + await _formatDart(); + await _formatJava(googleFormatterPath); + await _formatCppAndObjectiveC(); + + if (argResults['travis']) { + final bool modified = await _didModifyAnything(); + if (modified) { + throw ToolExit(1); + } + } + } + + Future _didModifyAnything() async { + final io.ProcessResult modifiedFiles = await processRunner + .runAndExitOnError('git', ['ls-files', '--modified'], + workingDir: packagesDir); + + print('\n\n'); + + if (modifiedFiles.stdout.isEmpty) { + print('All files formatted correctly.'); + return false; + } + + print('These files are not formatted correctly (see diff below):'); + LineSplitter.split(modifiedFiles.stdout) + .map((String line) => ' $line') + .forEach(print); + + print('\nTo fix run "pub global activate flutter_plugin_tools && ' + 'pub global run flutter_plugin_tools format" or copy-paste ' + 'this command into your terminal:'); + + print('patch -p1 <['diff'], workingDir: packagesDir); + print(diff.stdout); + print('DONE'); + return true; + } + + Future _formatCppAndObjectiveC() async { + print('Formatting all .cc, .cpp, .mm, .m, and .h files...'); + final Iterable allFiles = [] + ..addAll(await _getFilesWithExtension('.h')) + ..addAll(await _getFilesWithExtension('.m')) + ..addAll(await _getFilesWithExtension('.mm')) + ..addAll(await _getFilesWithExtension('.cc')) + ..addAll(await _getFilesWithExtension('.cpp')); + // Split this into multiple invocations to avoid a + // 'ProcessException: Argument list too long'. + final Iterable> batches = partition(allFiles, 100); + for (List batch in batches) { + await processRunner.runAndStream(argResults['clang-format'], + ['-i', '--style=Google']..addAll(batch), + workingDir: packagesDir, exitOnError: true); + } + } + + Future _formatJava(String googleFormatterPath) async { + print('Formatting all .java files...'); + final Iterable javaFiles = await _getFilesWithExtension('.java'); + await processRunner.runAndStream('java', + ['-jar', googleFormatterPath, '--replace']..addAll(javaFiles), + workingDir: packagesDir, exitOnError: true); + } + + Future _formatDart() async { + // This actually should be fine for non-Flutter Dart projects, no need to + // specifically shell out to dartfmt -w in that case. + print('Formatting all .dart files...'); + final Iterable dartFiles = await _getFilesWithExtension('.dart'); + if (dartFiles.isEmpty) { + print( + 'No .dart files to format. If you set the `--exclude` flag, most likey they were skipped'); + } else { + await processRunner.runAndStream( + 'flutter', ['format']..addAll(dartFiles), + workingDir: packagesDir, exitOnError: true); + } + } + + Future> _getFilesWithExtension(String extension) async => + getFiles() + .where((File file) => p.extension(file.path) == extension) + .map((File file) => file.path) + .toList(); + + Future _getGoogleFormatterPath() async { + final String javaFormatterPath = p.join( + p.dirname(p.fromUri(io.Platform.script)), + 'google-java-format-1.3-all-deps.jar'); + final File javaFormatterFile = fileSystem.file(javaFormatterPath); + + if (!javaFormatterFile.existsSync()) { + print('Downloading Google Java Format...'); + final http.Response response = await http.get(_googleFormatterUrl); + javaFormatterFile.writeAsBytesSync(response.bodyBytes); + } + + return javaFormatterPath; + } +} diff --git a/packages/fluttter_plugin_tools/lib/src/java_test_command.dart b/packages/fluttter_plugin_tools/lib/src/java_test_command.dart new file mode 100644 index 000000000000..cf605bfc5ce2 --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/java_test_command.dart @@ -0,0 +1,89 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class JavaTestCommand extends PluginCommand { + JavaTestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner); + + @override + final String name = 'java-test'; + + @override + final String description = 'Runs the Java tests of the example apps.\n\n' + 'Building the apks of the example apps is required before executing this' + 'command.'; + + static const String _gradleWrapper = 'gradlew'; + + @override + Future run() async { + checkSharding(); + final Stream examplesWithTests = getExamples().where( + (Directory d) => + isFlutterPackage(d, fileSystem) && + fileSystem + .directory(p.join(d.path, 'android', 'app', 'src', 'test')) + .existsSync()); + + final List failingPackages = []; + final List missingFlutterBuild = []; + await for (Directory example in examplesWithTests) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + print('\nRUNNING JAVA TESTS for $packageName'); + + final Directory androidDirectory = + fileSystem.directory(p.join(example.path, 'android')); + if (!fileSystem + .file(p.join(androidDirectory.path, _gradleWrapper)) + .existsSync()) { + print('ERROR: Run "flutter build apk" on example app of $packageName' + 'before executing tests.'); + missingFlutterBuild.add(packageName); + continue; + } + + final int exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + ['testDebugUnitTest', '--info'], + workingDir: androidDirectory); + if (exitCode != 0) { + failingPackages.add(packageName); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print( + 'The Java tests for the following packages are failing (see above for' + 'details):'); + for (String package in failingPackages) { + print(' * $package'); + } + } + if (missingFlutterBuild.isNotEmpty) { + print('Run "pub global run flutter_plugin_tools build-examples --apk" on' + 'the following packages before executing tests again:'); + for (String package in missingFlutterBuild) { + print(' * $package'); + } + } + + if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { + throw ToolExit(1); + } + + print('All Java tests successful!'); + } +} diff --git a/packages/fluttter_plugin_tools/lib/src/lint_podspecs_command.dart b/packages/fluttter_plugin_tools/lib/src/lint_podspecs_command.dart new file mode 100644 index 000000000000..68fd4b61dd66 --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/lint_podspecs_command.dart @@ -0,0 +1,146 @@ +// 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 'dart:convert'; +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'common.dart'; + +typedef void Print(Object object); + +/// Lint the CocoaPod podspecs, run the static analyzer on iOS/macOS plugin +/// platform code, and run unit tests. +/// +/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +class LintPodspecsCommand extends PluginCommand { + LintPodspecsCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), + Print print = print, + }) : _print = print, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addMultiOption('skip', + help: + 'Skip all linting for podspecs with this basename (example: federated plugins with placeholder podspecs)', + valueHelp: 'podspec_file_name'); + argParser.addMultiOption('ignore-warnings', + help: + 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs with this basename (example: plugins with known warnings)', + valueHelp: 'podspec_file_name'); + argParser.addMultiOption('no-analyze', + help: + 'Do not pass --analyze flag to "pod lib lint" for podspecs with this basename (example: plugins with known analyzer warnings)', + valueHelp: 'podspec_file_name'); + } + + @override + final String name = 'podspecs'; + + @override + List get aliases => ['podspec']; + + @override + final String description = + 'Runs "pod lib lint" on all iOS and macOS plugin podspecs.\n\n' + 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; + + final Platform platform; + + final Print _print; + + @override + Future run() async { + if (!platform.isMacOS) { + _print('Detected platform is not macOS, skipping podspec lint'); + return; + } + + checkSharding(); + + await processRunner.runAndExitOnError('which', ['pod'], + workingDir: packagesDir); + + _print('Starting podspec lint test'); + + final List failingPlugins = []; + for (File podspec in await _podspecsToLint()) { + if (!await _lintPodspec(podspec)) { + failingPlugins.add(p.basenameWithoutExtension(podspec.path)); + } + } + + _print('\n\n'); + if (failingPlugins.isNotEmpty) { + _print('The following plugins have podspec errors (see above):'); + failingPlugins.forEach((String plugin) { + _print(' * $plugin'); + }); + throw ToolExit(1); + } + } + + Future> _podspecsToLint() async { + final List podspecs = await getFiles().where((File entity) { + final String filePath = entity.path; + return p.extension(filePath) == '.podspec' && + !argResults['skip'].contains(p.basenameWithoutExtension(filePath)); + }).toList(); + + podspecs.sort( + (File a, File b) => p.basename(a.path).compareTo(p.basename(b.path))); + return podspecs; + } + + Future _lintPodspec(File podspec) async { + // Do not run the static analyzer on plugins with known analyzer issues. + final String podspecPath = podspec.path; + final bool runAnalyzer = !argResults['no-analyze'] + .contains(p.basenameWithoutExtension(podspecPath)); + + final String podspecBasename = p.basename(podspecPath); + if (runAnalyzer) { + _print('Linting and analyzing $podspecBasename'); + } else { + _print('Linting $podspecBasename'); + } + + // Lint plugin as framework (use_frameworks!). + final ProcessResult frameworkResult = await _runPodLint(podspecPath, + runAnalyzer: runAnalyzer, libraryLint: true); + _print(frameworkResult.stdout); + _print(frameworkResult.stderr); + + // Lint plugin as library. + final ProcessResult libraryResult = await _runPodLint(podspecPath, + runAnalyzer: runAnalyzer, libraryLint: false); + _print(libraryResult.stdout); + _print(libraryResult.stderr); + + return frameworkResult.exitCode == 0 && libraryResult.exitCode == 0; + } + + Future _runPodLint(String podspecPath, + {bool runAnalyzer, bool libraryLint}) async { + final bool allowWarnings = argResults['ignore-warnings'] + .contains(p.basenameWithoutExtension(podspecPath)); + final List arguments = [ + 'lib', + 'lint', + podspecPath, + if (allowWarnings) '--allow-warnings', + if (runAnalyzer) '--analyze', + if (libraryLint) '--use-libraries' + ]; + + return processRunner.run('pod', arguments, + workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); + } +} diff --git a/packages/fluttter_plugin_tools/lib/src/list_command.dart b/packages/fluttter_plugin_tools/lib/src/list_command.dart new file mode 100644 index 000000000000..7f94daac7096 --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/list_command.dart @@ -0,0 +1,60 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:file/file.dart'; + +import 'common.dart'; + +class ListCommand extends PluginCommand { + ListCommand(Directory packagesDir, FileSystem fileSystem) + : super(packagesDir, fileSystem) { + argParser.addOption( + _type, + defaultsTo: _plugin, + allowed: [_plugin, _example, _package, _file], + help: 'What type of file system content to list.', + ); + } + + static const String _type = 'type'; + static const String _plugin = 'plugin'; + static const String _example = 'example'; + static const String _package = 'package'; + static const String _file = 'file'; + + @override + final String name = 'list'; + + @override + final String description = 'Lists packages or files'; + + @override + Future run() async { + checkSharding(); + switch (argResults[_type]) { + case _plugin: + await for (Directory package in getPlugins()) { + print(package.path); + } + break; + case _example: + await for (Directory package in getExamples()) { + print(package.path); + } + break; + case _package: + await for (Directory package in getPackages()) { + print(package.path); + } + break; + case _file: + await for (File file in getFiles()) { + print(file.path); + } + break; + } + } +} diff --git a/packages/fluttter_plugin_tools/lib/src/main.dart b/packages/fluttter_plugin_tools/lib/src/main.dart new file mode 100644 index 000000000000..bb3f67c0a9e1 --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/main.dart @@ -0,0 +1,63 @@ +// 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:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; +import 'package:path/path.dart' as p; + +import 'analyze_command.dart'; +import 'build_examples_command.dart'; +import 'common.dart'; +import 'create_all_plugins_app_command.dart'; +import 'drive_examples_command.dart'; +import 'firebase_test_lab_command.dart'; +import 'format_command.dart'; +import 'java_test_command.dart'; +import 'lint_podspecs_command.dart'; +import 'list_command.dart'; +import 'test_command.dart'; +import 'version_check_command.dart'; +import 'xctest_command.dart'; + +void main(List args) { + final FileSystem fileSystem = const LocalFileSystem(); + + Directory packagesDir = fileSystem + .directory(p.join(fileSystem.currentDirectory.path, 'packages')); + + if (!packagesDir.existsSync()) { + if (p.basename(fileSystem.currentDirectory.path) == 'packages') { + packagesDir = fileSystem.currentDirectory; + } else { + print('Error: Cannot find a "packages" sub-directory'); + io.exit(1); + } + } + + final CommandRunner commandRunner = CommandRunner( + 'pub global run flutter_plugin_tools', + 'Productivity utils for hosting multiple plugins within one repository.') + ..addCommand(AnalyzeCommand(packagesDir, fileSystem)) + ..addCommand(BuildExamplesCommand(packagesDir, fileSystem)) + ..addCommand(CreateAllPluginsAppCommand(packagesDir, fileSystem)) + ..addCommand(DriveExamplesCommand(packagesDir, fileSystem)) + ..addCommand(FirebaseTestLabCommand(packagesDir, fileSystem)) + ..addCommand(FormatCommand(packagesDir, fileSystem)) + ..addCommand(JavaTestCommand(packagesDir, fileSystem)) + ..addCommand(LintPodspecsCommand(packagesDir, fileSystem)) + ..addCommand(ListCommand(packagesDir, fileSystem)) + ..addCommand(PublishPluginCommand(packagesDir, fileSystem)) + ..addCommand(TestCommand(packagesDir, fileSystem)) + ..addCommand(VersionCheckCommand(packagesDir, fileSystem)) + ..addCommand(XCTestCommand(packagesDir, fileSystem)); + + commandRunner.run(args).catchError((Object e) { + final ToolExit toolExit = e; + io.exit(toolExit.exitCode); + }, test: (Object e) => e is ToolExit); +} diff --git a/packages/fluttter_plugin_tools/lib/src/publish_plugin_command.dart b/packages/fluttter_plugin_tools/lib/src/publish_plugin_command.dart new file mode 100644 index 000000000000..55c48f5484e5 --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/publish_plugin_command.dart @@ -0,0 +1,227 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; + +import 'common.dart'; + +/// Wraps pub publish with a few niceties used by the flutter/plugin team. +/// +/// 1. Checks for any modified files in git and refuses to publish if there's an +/// issue. +/// 2. Tags the release with the format -v. +/// 3. Pushes the release to a remote. +/// +/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full +/// usage information. +/// +/// [processRunner], [print], and [stdin] can be overriden for easier testing. +class PublishPluginCommand extends PluginCommand { + PublishPluginCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + Print print = print, + Stdin stdinput, + }) : _print = print, + _stdin = stdinput ?? stdin, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + _packageOption, + help: 'The package to publish.' + 'If the package directory name is different than its pubspec.yaml name, then this should specify the directory.', + ); + argParser.addMultiOption(_pubFlagsOption, + help: + 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); + argParser.addFlag( + _tagReleaseOption, + help: 'Whether or not to tag the release.', + defaultsTo: true, + negatable: true, + ); + argParser.addFlag( + _pushTagsOption, + help: + 'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.', + defaultsTo: true, + negatable: true, + ); + argParser.addOption( + _remoteOption, + help: + 'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.', + // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. + defaultsTo: 'upstream', + ); + } + + static const String _packageOption = 'package'; + static const String _tagReleaseOption = 'tag-release'; + static const String _pushTagsOption = 'push-tags'; + static const String _pubFlagsOption = 'pub-publish-flags'; + static const String _remoteOption = 'remote'; + + // Version tags should follow -v. For example, + // `flutter_plugin_tools-v0.0.24`. + static const String _tagFormat = '%PACKAGE%-v%VERSION%'; + + @override + final String name = 'publish-plugin'; + + @override + final String description = + 'Attempts to publish the given plugin and tag its release on GitHub.'; + + final Print _print; + final Stdin _stdin; + // The directory of the actual package that we are publishing. + Directory _packageDir; + StreamSubscription _stdinSubscription; + + @override + Future run() async { + checkSharding(); + _print('Checking local repo...'); + _packageDir = _checkPackageDir(); + await _checkGitStatus(); + final bool shouldPushTag = argResults[_pushTagsOption]; + final String remote = argResults[_remoteOption]; + String remoteUrl; + if (shouldPushTag) { + remoteUrl = await _verifyRemote(remote); + } + _print('Local repo is ready!'); + + await _publish(); + _print('Package published!'); + if (!argResults[_tagReleaseOption]) { + return await _finishSuccesfully(); + } + + _print('Tagging release...'); + final String tag = _getTag(); + await processRunner.runAndExitOnError('git', ['tag', tag], + workingDir: _packageDir); + if (!shouldPushTag) { + return await _finishSuccesfully(); + } + + _print('Pushing tag to $remote...'); + await _pushTagToRemote(remote: remote, tag: tag, remoteUrl: remoteUrl); + await _finishSuccesfully(); + } + + Future _finishSuccesfully() async { + await _stdinSubscription.cancel(); + _print('Done!'); + } + + Directory _checkPackageDir() { + final String package = argResults[_packageOption]; + if (package == null) { + _print( + 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); + throw ToolExit(1); + } + final Directory _packageDir = packagesDir.childDirectory(package); + if (!_packageDir.existsSync()) { + _print('${_packageDir.absolute.path} does not exist.'); + throw ToolExit(1); + } + if (!isFlutterPackage(_packageDir, fileSystem)) { + _print('${_packageDir.absolute.path} is not a flutter package.'); + throw ToolExit(1); + } + return _packageDir; + } + + Future _checkGitStatus() async { + if (!await GitDir.isGitDir(packagesDir.path)) { + _print('$packagesDir is not a valid Git repository.'); + throw ToolExit(1); + } + + final ProcessResult statusResult = await processRunner.runAndExitOnError( + 'git', + [ + 'status', + '--porcelain', + '--ignored', + _packageDir.absolute.path + ], + workingDir: _packageDir); + final String statusOutput = statusResult.stdout; + if (statusOutput.isNotEmpty) { + _print( + "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" + '$statusOutput\n' + 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); + throw ToolExit(1); + } + } + + Future _verifyRemote(String remote) async { + final ProcessResult remoteInfo = await processRunner.runAndExitOnError( + 'git', ['remote', 'get-url', remote], + workingDir: _packageDir); + return remoteInfo.stdout; + } + + Future _publish() async { + final List publishFlags = argResults[_pubFlagsOption]; + _print( + 'Running `pub publish ${publishFlags.join(' ')}` in ${_packageDir.absolute.path}...\n'); + final Process publish = await processRunner.start( + 'flutter', ['pub', 'publish'] + publishFlags, + workingDirectory: _packageDir); + publish.stdout + .transform(utf8.decoder) + .listen((String data) => _print(data)); + publish.stderr + .transform(utf8.decoder) + .listen((String data) => _print(data)); + _stdinSubscription = _stdin + .transform(utf8.decoder) + .listen((String data) => publish.stdin.writeln(data)); + final int result = await publish.exitCode; + if (result != 0) { + _print('Publish failed. Exiting.'); + throw ToolExit(result); + } + } + + String _getTag() { + final File pubspecFile = + fileSystem.file(p.join(_packageDir.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final String name = pubspecYaml['name']; + final String version = pubspecYaml['version']; + // We should have failed to publish if these were unset. + assert(name.isNotEmpty && version.isNotEmpty); + return _tagFormat + .replaceAll('%PACKAGE%', name) + .replaceAll('%VERSION%', version); + } + + Future _pushTagToRemote( + {@required String remote, + @required String tag, + @required String remoteUrl}) async { + assert(remote != null && tag != null && remoteUrl != null); + _print('Ready to push $tag to $remoteUrl (y/n)?'); + final String input = _stdin.readLineSync(); + if (input.toLowerCase() != 'y') { + _print('Tag push canceled.'); + throw ToolExit(1); + } + + await processRunner.runAndExitOnError('git', ['push', remote, tag], + workingDir: packagesDir); + } +} diff --git a/packages/fluttter_plugin_tools/lib/src/test_command.dart b/packages/fluttter_plugin_tools/lib/src/test_command.dart new file mode 100644 index 000000000000..e938168cfa89 --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/test_command.dart @@ -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. + +import 'dart:async'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class TestCommand extends PluginCommand { + TestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Runs the tests in Dart VM with the given experiments enabled.', + ); + } + + @override + final String name = 'test'; + + @override + final String description = 'Runs the Dart tests for all packages.\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + final List failingPackages = []; + await for (Directory packageDir in getPackages()) { + final String packageName = + p.relative(packageDir.path, from: packagesDir.path); + if (!fileSystem.directory(p.join(packageDir.path, 'test')).existsSync()) { + print('SKIPPING $packageName - no test subdirectory'); + continue; + } + + print('RUNNING $packageName tests...'); + + final String enableExperiment = argResults[kEnableExperiment]; + + // `flutter test` automatically gets packages. `pub run test` does not. :( + int exitCode = 0; + if (isFlutterPackage(packageDir, fileSystem)) { + final List args = [ + 'test', + '--color', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ]; + + if (isWebPlugin(packageDir, fileSystem)) { + args.add('--platform=chrome'); + } + exitCode = await processRunner.runAndStream( + 'flutter', + args, + workingDir: packageDir, + ); + } else { + exitCode = await processRunner.runAndStream( + 'pub', + ['get'], + workingDir: packageDir, + ); + if (exitCode == 0) { + exitCode = await processRunner.runAndStream( + 'pub', + [ + 'run', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + 'test', + ], + workingDir: packageDir, + ); + } + } + if (exitCode != 0) { + failingPackages.add(packageName); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print('Tests for the following packages are failing (see above):'); + failingPackages.forEach((String package) { + print(' * $package'); + }); + throw ToolExit(1); + } + + print('All tests are passing!'); + } +} diff --git a/packages/fluttter_plugin_tools/lib/src/version_check_command.dart b/packages/fluttter_plugin_tools/lib/src/version_check_command.dart new file mode 100644 index 000000000000..2c6b92bbcb7a --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/version_check_command.dart @@ -0,0 +1,220 @@ +// 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 'dart:io' as io; + +import 'package:meta/meta.dart'; +import 'package:colorize/colorize.dart'; +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:yaml/yaml.dart'; + +import 'common.dart'; + +const String _kBaseSha = 'base_sha'; + +class GitVersionFinder { + GitVersionFinder(this.baseGitDir, this.baseSha); + + final GitDir baseGitDir; + final String baseSha; + + static bool isPubspec(String file) { + return file.trim().endsWith('pubspec.yaml'); + } + + Future> getChangedPubSpecs() async { + final io.ProcessResult changedFilesCommand = await baseGitDir + .runCommand(['diff', '--name-only', '$baseSha', 'HEAD']); + final List changedFiles = + changedFilesCommand.stdout.toString().split('\n'); + return changedFiles.where(isPubspec).toList(); + } + + Future getPackageVersion(String pubspecPath, String gitRef) async { + final io.ProcessResult gitShow = + await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); + final String fileContent = gitShow.stdout; + final String versionString = loadYaml(fileContent)['version']; + return versionString == null ? null : Version.parse(versionString); + } +} + +enum NextVersionType { + BREAKING_MAJOR, + MAJOR_NULLSAFETY_PRE_RELEASE, + MINOR_NULLSAFETY_PRE_RELEASE, + MINOR, + PATCH, + RELEASE, +} + +Version getNextNullSafetyPreRelease(Version current, Version next) { + String nextNullsafetyPrerelease = 'nullsafety'; + if (current.isPreRelease && + current.preRelease.first is String && + current.preRelease.first == 'nullsafety') { + if (current.preRelease.length == 1) { + nextNullsafetyPrerelease = 'nullsafety.1'; + } else if (current.preRelease.length == 2 && + current.preRelease.last is int) { + nextNullsafetyPrerelease = 'nullsafety.${current.preRelease.last + 1}'; + } + } + return Version( + next.major, + next.minor, + next.patch, + pre: nextNullsafetyPrerelease, + ); +} + +@visibleForTesting +Map getAllowedNextVersions( + Version masterVersion, Version headVersion) { + final Version nextNullSafetyMajor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMajor); + final Version nextNullSafetyMinor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); + final Map allowedNextVersions = + { + masterVersion.nextMajor: NextVersionType.BREAKING_MAJOR, + nextNullSafetyMajor: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE, + nextNullSafetyMinor: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE, + masterVersion.nextMinor: NextVersionType.MINOR, + masterVersion.nextPatch: NextVersionType.PATCH, + }; + + if (masterVersion.major < 1 && headVersion.major < 1) { + int nextBuildNumber = -1; + if (masterVersion.build.isEmpty) { + nextBuildNumber = 1; + } else { + final int currentBuildNumber = masterVersion.build.first; + nextBuildNumber = currentBuildNumber + 1; + } + final Version preReleaseVersion = Version( + masterVersion.major, + masterVersion.minor, + masterVersion.patch, + build: nextBuildNumber.toString(), + ); + allowedNextVersions.clear(); + allowedNextVersions[masterVersion.nextMajor] = NextVersionType.RELEASE; + allowedNextVersions[masterVersion.nextMinor] = + NextVersionType.BREAKING_MAJOR; + allowedNextVersions[masterVersion.nextPatch] = NextVersionType.MINOR; + allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; + + final Version nextNullSafetyMajor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); + final Version nextNullSafetyMinor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextPatch); + + allowedNextVersions[nextNullSafetyMajor] = + NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE; + allowedNextVersions[nextNullSafetyMinor] = + NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE; + } + return allowedNextVersions; +} + +class VersionCheckCommand extends PluginCommand { + VersionCheckCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + this.gitDir, + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption(_kBaseSha); + } + + /// The git directory to use. By default it uses the parent directory. + /// + /// This can be mocked for testing. + final GitDir gitDir; + + @override + final String name = 'version-check'; + + @override + final String description = + 'Checks if the versions of the plugins have been incremented per pub specification.\n\n' + 'This command requires "pub" and "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + + final String rootDir = packagesDir.parent.absolute.path; + final String baseSha = argResults[_kBaseSha]; + + GitDir baseGitDir = gitDir; + if (baseGitDir == null) { + if (!await GitDir.isGitDir(rootDir)) { + print('$rootDir is not a valid Git repository.'); + throw ToolExit(2); + } + baseGitDir = await GitDir.fromExisting(rootDir); + } + + final GitVersionFinder gitVersionFinder = + GitVersionFinder(baseGitDir, baseSha); + + final List changedPubspecs = + await gitVersionFinder.getChangedPubSpecs(); + + for (final String pubspecPath in changedPubspecs) { + try { + final File pubspecFile = fileSystem.file(pubspecPath); + if (!pubspecFile.existsSync()) { + continue; + } + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec.publishTo == 'none') { + continue; + } + + final Version masterVersion = + await gitVersionFinder.getPackageVersion(pubspecPath, baseSha); + final Version headVersion = + await gitVersionFinder.getPackageVersion(pubspecPath, 'HEAD'); + if (headVersion == null) { + continue; // Example apps don't have versions + } + + final Map allowedNextVersions = + getAllowedNextVersions(masterVersion, headVersion); + + if (!allowedNextVersions.containsKey(headVersion)) { + final String error = '$pubspecPath incorrectly updated version.\n' + 'HEAD: $headVersion, master: $masterVersion.\n' + 'Allowed versions: $allowedNextVersions'; + final Colorize redError = Colorize(error)..red(); + print(redError); + throw ToolExit(1); + } + + bool isPlatformInterface = pubspec.name.endsWith("_platform_interface"); + if (isPlatformInterface && + allowedNextVersions[headVersion] == + NextVersionType.BREAKING_MAJOR) { + final String error = '$pubspecPath breaking change detected.\n' + 'Breaking changes to platform interfaces are strongly discouraged.\n'; + final Colorize redError = Colorize(error)..red(); + print(redError); + throw ToolExit(1); + } + } on io.ProcessException { + print('Unable to find pubspec in master for $pubspecPath.' + ' Safe to ignore if the project is new.'); + } + } + + print('No version check errors found!'); + } +} diff --git a/packages/fluttter_plugin_tools/lib/src/xctest_command.dart b/packages/fluttter_plugin_tools/lib/src/xctest_command.dart new file mode 100644 index 000000000000..d90b7a8fbfea --- /dev/null +++ b/packages/fluttter_plugin_tools/lib/src/xctest_command.dart @@ -0,0 +1,216 @@ +// 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 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +const String _kiOSDestination = 'ios-destination'; +const String _kTarget = 'target'; +const String _kSkip = 'skip'; +const String _kXcodeBuildCommand = 'xcodebuild'; +const String _kXCRunCommand = 'xcrun'; +const String _kFoundNoSimulatorsMessage = + 'Cannot find any available simulators, tests failed'; + +/// The command to run iOS' XCTests in plugins, this should work for both XCUnitTest and XCUITest targets. +/// The tests target have to be added to the xcode project of the example app. Usually at "example/ios/Runner.xcodeproj". +/// The command takes a "-target" argument which has to match the target of the test target. +/// For information on how to add test target in an xcode project, see https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/UnitTesting.html +class XCTestCommand extends PluginCommand { + XCTestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + _kiOSDestination, + help: + 'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n' + 'this is passed to the `-destination` argument in xcodebuild command.\n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', + ); + argParser.addOption(_kTarget, + help: 'The test target.\n' + 'This is the xcode project test target. This is passed to the `-scheme` argument in the xcodebuild command. \n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the scheme'); + argParser.addMultiOption(_kSkip, + help: 'Plugins to skip while running this command. \n'); + } + + @override + final String name = 'xctest'; + + @override + final String description = 'Runs the xctests in the iOS example apps.\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + if (argResults[_kTarget] == null) { + // TODO(cyanglaz): Automatically find all the available testing schemes if this argument is not specified. + // https://github.com/flutter/flutter/issues/68419 + print('--$_kTarget must be specified'); + throw ToolExit(1); + } + + String destination = argResults[_kiOSDestination]; + if (destination == null) { + String simulatorId = await _findAvailableIphoneSimulator(); + if (simulatorId == null) { + print(_kFoundNoSimulatorsMessage); + throw ToolExit(1); + } + destination = 'id=$simulatorId'; + } + + checkSharding(); + + final String target = argResults[_kTarget]; + final List skipped = argResults[_kSkip]; + + List failingPackages = []; + await for (Directory plugin in getPlugins()) { + // Start running for package. + final String packageName = + p.relative(plugin.path, from: packagesDir.path); + print('Start running for $packageName ...'); + if (!isIosPlugin(plugin, fileSystem)) { + print('iOS is not supported by this plugin.'); + print('\n\n'); + continue; + } + if (skipped.contains(packageName)) { + print('$packageName was skipped with the --skip flag.'); + print('\n\n'); + continue; + } + for (Directory example in getExamplesForPlugin(plugin)) { + // Look for the test scheme in the example app. + print('Look for target named: $_kTarget ...'); + final List findSchemeArgs = [ + '-project', + 'ios/Runner.xcodeproj', + '-list', + '-json' + ]; + final String completeFindSchemeCommand = + '$_kXcodeBuildCommand ${findSchemeArgs.join(' ')}'; + print(completeFindSchemeCommand); + final io.ProcessResult xcodeprojListResult = await processRunner + .run(_kXcodeBuildCommand, findSchemeArgs, workingDir: example); + if (xcodeprojListResult.exitCode != 0) { + print('Error occurred while running "$completeFindSchemeCommand":\n' + '${xcodeprojListResult.stderr}'); + failingPackages.add(packageName); + print('\n\n'); + continue; + } + + final String xcodeprojListOutput = xcodeprojListResult.stdout; + Map xcodeprojListOutputJson = + jsonDecode(xcodeprojListOutput); + if (!xcodeprojListOutputJson['project']['targets'].contains(target)) { + failingPackages.add(packageName); + print('$target not configured for $packageName, test failed.'); + print( + 'Please check the scheme for the test target if it matches the name $target.\n' + 'If this plugin does not have an XCTest target, use the $_kSkip flag in the $name command to skip the plugin.'); + print('\n\n'); + continue; + } + // Found the scheme, running tests + print('Running XCTests:$target for $packageName ...'); + final List xctestArgs = [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + target, + '-destination', + destination, + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ]; + final String completeTestCommand = + '$_kXcodeBuildCommand ${xctestArgs.join(' ')}'; + print(completeTestCommand); + final int exitCode = await processRunner + .runAndStream(_kXcodeBuildCommand, xctestArgs, workingDir: example); + if (exitCode == 0) { + print('Successfully ran xctest for $packageName'); + } else { + failingPackages.add(packageName); + } + } + } + + // Command end, print reports. + if (failingPackages.isEmpty) { + print("All XCTests have passed!"); + } else { + print( + 'The following packages are failing XCTests (see above for details):'); + for (String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + } + + Future _findAvailableIphoneSimulator() async { + // Find the first available destination if not specified. + final List findSimulatorsArguments = [ + 'simctl', + 'list', + '--json' + ]; + final String findSimulatorCompleteCommand = + '$_kXCRunCommand ${findSimulatorsArguments.join(' ')}'; + print('Looking for available simulators...'); + print(findSimulatorCompleteCommand); + final io.ProcessResult findSimulatorsResult = + await processRunner.run(_kXCRunCommand, findSimulatorsArguments); + if (findSimulatorsResult.exitCode != 0) { + print('Error occurred while running "$findSimulatorCompleteCommand":\n' + '${findSimulatorsResult.stderr}'); + throw ToolExit(1); + } + final Map simulatorListJson = + jsonDecode(findSimulatorsResult.stdout); + final List runtimes = simulatorListJson['runtimes']; + final Map devices = simulatorListJson['devices']; + if (runtimes.isEmpty || devices.isEmpty) { + return null; + } + String id; + // Looking for runtimes, trying to find one with highest OS version. + for (Map runtimeMap in runtimes.reversed) { + if (!runtimeMap['name'].contains('iOS')) { + continue; + } + final String runtimeID = runtimeMap['identifier']; + final List devicesForRuntime = devices[runtimeID]; + if (devicesForRuntime.isEmpty) { + continue; + } + // Looking for runtimes, trying to find latest version of device. + for (Map device in devicesForRuntime.reversed) { + if (device['availabilityError'] != null || + (device['isAvailable'] as bool == false)) { + continue; + } + id = device['udid']; + print('device selected: $device'); + return id; + } + } + return null; + } +} diff --git a/packages/fluttter_plugin_tools/pubspec.yaml b/packages/fluttter_plugin_tools/pubspec.yaml new file mode 100644 index 000000000000..f8b452910236 --- /dev/null +++ b/packages/fluttter_plugin_tools/pubspec.yaml @@ -0,0 +1,30 @@ +name: flutter_plugin_tools +description: Productivity utils for hosting multiple plugins within one repository. +homepage: https://github.com/flutter/plugin_tools +version: 0.0.45+1 + +dependencies: + args: "^1.4.3" + path: "^1.6.1" + http: "^0.12.1" + async: "^2.0.7" + yaml: "^2.1.15" + quiver: "^2.0.2" + pub_semver: ^1.4.2 + colorize: ^2.0.0 + git: ^1.0.0 + platform: ^2.2.0 + pubspec_parse: "^0.1.4" + test: ^1.6.4 + meta: ^1.1.7 + file: ^5.0.10 + uuid: ^2.0.4 + http_multi_server: ^2.2.0 + +dev_dependencies: + matcher: ^0.12.6 + mockito: ^4.1.1 + pedantic: 1.8.0 + +environment: + sdk: ">=2.3.0 <3.0.0" diff --git a/packages/fluttter_plugin_tools/test/analyze_command_test.dart b/packages/fluttter_plugin_tools/test/analyze_command_test.dart new file mode 100644 index 000000000000..9e7a42bbb680 --- /dev/null +++ b/packages/fluttter_plugin_tools/test/analyze_command_test.dart @@ -0,0 +1,93 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/analyze_command.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + RecordingProcessRunner processRunner; + CommandRunner runner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final AnalyzeCommand analyzeCommand = AnalyzeCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('analyze_command', 'Test for analyze_command'); + runner.addCommand(analyzeCommand); + }); + + tearDown(() { + mockPackagesDir.deleteSync(recursive: true); + }); + + test('analyzes all packages', () async { + final Directory plugin1Dir = await createFakePlugin('a'); + final Directory plugin2Dir = await createFakePlugin('b'); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('pub', ['global', 'activate', 'tuneup'], + mockPackagesDir.path), + ProcessCall('flutter', ['packages', 'get'], plugin1Dir.path), + ProcessCall('flutter', ['packages', 'get'], plugin2Dir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + plugin1Dir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + plugin2Dir.path), + ])); + }); + + group('verifies analysis settings', () { + test('fails analysis_options.yaml', () async { + await createFakePlugin('foo', withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + await expectLater(() => runner.run(['analyze']), + throwsA(const TypeMatcher())); + }); + + test('fails .analysis_options', () async { + await createFakePlugin('foo', withExtraFiles: >[ + ['.analysis_options'] + ]); + + await expectLater(() => runner.run(['analyze']), + throwsA(const TypeMatcher())); + }); + + test('takes an allow list', () async { + final Directory pluginDir = + await createFakePlugin('foo', withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze', '--custom-analysis', 'foo']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('pub', ['global', 'activate', 'tuneup'], + mockPackagesDir.path), + ProcessCall('flutter', ['packages', 'get'], pluginDir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + pluginDir.path), + ])); + }); + }); +} diff --git a/packages/fluttter_plugin_tools/test/build_examples_command_test.dart b/packages/fluttter_plugin_tools/test/build_examples_command_test.dart new file mode 100644 index 000000000000..eaf5049dcc02 --- /dev/null +++ b/packages/fluttter_plugin_tools/test/build_examples_command_test.dart @@ -0,0 +1,470 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/build_examples_command.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test build_example_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final BuildExamplesCommand command = BuildExamplesCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner( + 'build_examples_command', 'Test for build_example_command'); + runner.addCommand(command); + cleanupPackages(); + }); + + test('building for iOS when plugin is not set up for iOS results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ipa', '--no-macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING IPA for $packageName', + 'iOS is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for ios', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'build-examples', + '--ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING IPA for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Linux when plugin is not set up for Linux results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--linux']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Linux for $packageName', + 'Linux is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --linux with no + // Linux implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for Linux', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--linux']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Linux for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'linux'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('building for macos with no implementation results in no-op', + () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['example', 'test'], + ]); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING macOS for $packageName', + '\macOS is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + test('building for macos', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ['example', 'macos', 'macos.swift'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING macOS for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['pub', 'get'], + pluginExampleDirectory.path), + ProcessCall(flutterCommand, ['build', 'macos'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Windows when plugin is not set up for Windows results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isWindowsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--windows']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Windows for $packageName', + 'Windows is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for windows', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isWindowsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--windows']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Windows for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'windows'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Android when plugin is not set up for Android results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--apk', '--no-ipa']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING APK for $packageName', + 'Android is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for android', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'build-examples', + '--apk', + '--no-ipa', + '--no-macos', + ]); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING APK for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'apk'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('enable-experiment flag for Android', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'build-examples', + '--apk', + '--no-ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + ['build', 'apk', '--enable-experiment=exp1'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('enable-experiment flag for ios', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'build-examples', + '--ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + }); +} diff --git a/packages/fluttter_plugin_tools/test/common_test.dart b/packages/fluttter_plugin_tools/test/common_test.dart new file mode 100644 index 000000000000..b3504c2358d9 --- /dev/null +++ b/packages/fluttter_plugin_tools/test/common_test.dart @@ -0,0 +1,100 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + RecordingProcessRunner processRunner; + CommandRunner runner; + List plugins; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + plugins = []; + final SamplePluginCommand samplePluginCommand = SamplePluginCommand( + plugins, + mockPackagesDir, + mockFileSystem, + processRunner: processRunner, + ); + runner = + CommandRunner('common_command', 'Test for common functionality'); + runner.addCommand(samplePluginCommand); + }); + + tearDown(() { + mockPackagesDir.deleteSync(recursive: true); + }); + + test('all plugins from file system', () async { + final Directory plugin1 = createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run(['sample']); + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('exclude plugins when plugins flag is specified', () async { + createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run( + ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); + expect(plugins, unorderedEquals([plugin2.path])); + }); + + test('exclude plugins when plugins flag isn\'t specified', () async { + createFakePlugin('plugin1'); + createFakePlugin('plugin2'); + await runner.run(['sample', '--exclude=plugin1,plugin2']); + expect(plugins, unorderedEquals([])); + }); + + test('exclude federated plugins when plugins flag is specified', () async { + createFakePlugin('plugin1', parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run([ + 'sample', + '--plugins=federated/plugin1,plugin2', + '--exclude=federated/plugin1' + ]); + expect(plugins, unorderedEquals([plugin2.path])); + }); + + test('exclude entire federated plugins when plugins flag is specified', + () async { + createFakePlugin('plugin1', parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run([ + 'sample', + '--plugins=federated/plugin1,plugin2', + '--exclude=federated' + ]); + expect(plugins, unorderedEquals([plugin2.path])); + }); +} + +class SamplePluginCommand extends PluginCommand { + SamplePluginCommand( + this.plugins_, + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner); + + List plugins_; + + @override + final String name = 'sample'; + + @override + final String description = 'sample command'; + + @override + Future run() async { + await for (Directory package in getPlugins()) { + this.plugins_.add(package.path); + } + } +} diff --git a/packages/fluttter_plugin_tools/test/drive_examples_command_test.dart b/packages/fluttter_plugin_tools/test/drive_examples_command_test.dart new file mode 100644 index 000000000000..f4bdd95c1664 --- /dev/null +++ b/packages/fluttter_plugin_tools/test/drive_examples_command_test.dart @@ -0,0 +1,505 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test drive_example_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final DriveExamplesCommand command = DriveExamplesCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner( + 'drive_examples_command', 'Test for drive_example_command'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('driving under folder "test"', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], + isIosPlugin: true, + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving under folder "test_driver"', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving under folder "test_driver" when test files are missing"', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await expectLater( + () => runCapturingPrint(runner, ['drive-examples']), + throwsA(const TypeMatcher())); + }); + + test( + 'driving under folder "test_driver" when targets are under "integration_test"', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'integration_test.dart'], + ['example', 'integration_test', 'bar_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'ignore_me.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String driverTestPath = p.join('test_driver', 'integration_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'bar_test.dart'), + ], + pluginExampleDirectory.path), + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'foo_test.dart'), + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support Linux is a no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --linux on a non-Linux + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Linux plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isLinuxPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'linux', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport macOS is a no-op', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ]); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + test('driving on a macOS plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ['example', 'macos', 'macos.swift'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'macos', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport windows is a no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --windows on a non-windows + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Windows plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isWindowsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'windows', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support mobile is no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('enable-experiment flag', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], + isIosPlugin: true, + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'drive-examples', + '--enable-experiment=exp1', + ]); + + String deviceTestPath = p.join('test', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--enable-experiment=exp1', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + }); +} diff --git a/packages/fluttter_plugin_tools/test/firebase_test_lab_test.dart b/packages/fluttter_plugin_tools/test/firebase_test_lab_test.dart new file mode 100644 index 000000000000..97b977619d57 --- /dev/null +++ b/packages/fluttter_plugin_tools/test/firebase_test_lab_test.dart @@ -0,0 +1,256 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$FirebaseTestLabCommand', () { + final List printedMessages = []; + CommandRunner runner; + RecordingProcessRunner processRunner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final FirebaseTestLabCommand command = FirebaseTestLabCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, + print: (Object message) => printedMessages.add(message.toString())); + + runner = CommandRunner( + 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); + runner.addCommand(command); + }); + + tearDown(() { + printedMessages.clear(); + }); + + test('retries gcloud set', () async { + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(1); + processRunner.processToReturn = mockProcess; + createFakePlugin('plugin', withExtraFiles: >[ + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + await expectLater( + () => runCapturingPrint(runner, ['firebase-test-lab']), + throwsA(const TypeMatcher())); + expect( + printedMessages, + contains( + "\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.")); + }); + + test('runs e2e tests', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['test', 'plugin_test.dart'], + ['test', 'plugin_e2e.dart'], + ['should_not_run_e2e.dart'], + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'should_not_run.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + ]); + + expect( + printedMessages, + orderedEquals([ + '\nRUNNING FIREBASE TEST LAB TESTS for plugin', + '\nFirebase project configured.', + '\n\n', + 'All Firebase Test Lab tests successful!', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ]), + ); + }); + + test('experimental flag', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['test', 'plugin_test.dart'], + ['test', 'plugin_e2e.dart'], + ['should_not_run_e2e.dart'], + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'should_not_run.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + + await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--test-run-id', + 'testRunId', + '--enable-experiment=exp1', + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ]), + ); + + cleanupPackages(); + }); + }); +} diff --git a/packages/fluttter_plugin_tools/test/lint_podspecs_command_test.dart b/packages/fluttter_plugin_tools/test/lint_podspecs_command_test.dart new file mode 100644 index 000000000000..49d6ad4d8e20 --- /dev/null +++ b/packages/fluttter_plugin_tools/test/lint_podspecs_command_test.dart @@ -0,0 +1,202 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$LintPodspecsCommand', () { + CommandRunner runner; + MockPlatform mockPlatform; + final RecordingProcessRunner processRunner = RecordingProcessRunner(); + List printedMessages; + + setUp(() { + initializeFakePackages(); + + printedMessages = []; + mockPlatform = MockPlatform(); + when(mockPlatform.isMacOS).thenReturn(true); + final LintPodspecsCommand command = LintPodspecsCommand( + mockPackagesDir, + mockFileSystem, + processRunner: processRunner, + platform: mockPlatform, + print: (Object message) => printedMessages.add(message.toString()), + ); + + runner = + CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); + runner.addCommand(command); + final MockProcess mockLintProcess = MockProcess(); + mockLintProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockLintProcess; + processRunner.recordedCalls.clear(); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('only runs on macOS', () async { + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + when(mockPlatform.isMacOS).thenReturn(false); + await runner.run(['podspecs']); + + expect( + processRunner.recordedCalls, + equals([]), + ); + }); + + test('runs pod lib lint on a podspec', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['ios', 'plugin1.podspec'], + ['bogus.dart'], // Ignore non-podspecs. + ]); + + processRunner.resultStdout = 'Foo'; + processRunner.resultStderr = 'Bar'; + + await runner.run(['podspecs']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--analyze', + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--analyze', + ], + mockPackagesDir.path), + ]), + ); + + expect( + printedMessages, contains('Linting and analyzing plugin1.podspec')); + expect(printedMessages, contains('Foo')); + expect(printedMessages, contains('Bar')); + }); + + test('skips podspecs with known issues', () async { + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'] + ]); + createFakePlugin('plugin2', withExtraFiles: >[ + ['plugin2.podspec'] + ]); + + await runner + .run(['podspecs', '--skip=plugin1', '--skip=plugin2']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ]), + ); + }); + + test('skips analyzer for podspecs with known warnings', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + await runner.run(['podspecs', '--no-analyze=plugin1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + ], + mockPackagesDir.path), + ]), + ); + + expect(printedMessages, contains('Linting plugin1.podspec')); + }); + + test('allow warnings for podspecs with known warnings', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + await runner.run(['podspecs', '--ignore-warnings=plugin1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--allow-warnings', + '--analyze', + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--allow-warnings', + '--analyze', + ], + mockPackagesDir.path), + ]), + ); + + expect( + printedMessages, contains('Linting and analyzing plugin1.podspec')); + }); + }); +} + +class MockPlatform extends Mock implements Platform {} diff --git a/packages/fluttter_plugin_tools/test/list_command_test.dart b/packages/fluttter_plugin_tools/test/list_command_test.dart new file mode 100644 index 000000000000..478625283dd0 --- /dev/null +++ b/packages/fluttter_plugin_tools/test/list_command_test.dart @@ -0,0 +1,198 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/list_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$ListCommand', () { + CommandRunner runner; + + setUp(() { + initializeFakePackages(); + final ListCommand command = ListCommand(mockPackagesDir, mockFileSystem); + + runner = CommandRunner('list_test', 'Test for $ListCommand'); + runner.addCommand(command); + }); + + test('lists plugins', () async { + createFakePlugin('plugin1'); + createFakePlugin('plugin2'); + + final List plugins = + await runCapturingPrint(runner, ['list', '--type=plugin']); + + expect( + plugins, + orderedEquals([ + '/packages/plugin1', + '/packages/plugin2', + ]), + ); + + cleanupPackages(); + }); + + test('lists examples', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=example']); + + expect( + examples, + orderedEquals([ + '/packages/plugin1/example', + '/packages/plugin2/example/example1', + '/packages/plugin2/example/example2', + ]), + ); + + cleanupPackages(); + }); + + test('lists packages', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List packages = + await runCapturingPrint(runner, ['list', '--type=package']); + + expect( + packages, + unorderedEquals([ + '/packages/plugin1', + '/packages/plugin1/example', + '/packages/plugin2', + '/packages/plugin2/example/example1', + '/packages/plugin2/example/example2', + '/packages/plugin3', + ]), + ); + + cleanupPackages(); + }); + + test('lists files', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=file']); + + expect( + examples, + unorderedEquals([ + '/packages/plugin1/pubspec.yaml', + '/packages/plugin1/example/pubspec.yaml', + '/packages/plugin2/pubspec.yaml', + '/packages/plugin2/example/example1/pubspec.yaml', + '/packages/plugin2/example/example2/pubspec.yaml', + '/packages/plugin3/pubspec.yaml', + ]), + ); + + cleanupPackages(); + }); + + test('lists plugins using federated plugin layout', () async { + createFakePlugin('plugin1'); + + // Create a federated plugin by creating a directory under the packages + // directory with several packages underneath. + final Directory federatedPlugin = + mockPackagesDir.childDirectory('my_plugin')..createSync(); + final Directory clientLibrary = + federatedPlugin.childDirectory('my_plugin')..createSync(); + createFakePubspec(clientLibrary); + final Directory webLibrary = + federatedPlugin.childDirectory('my_plugin_web')..createSync(); + createFakePubspec(webLibrary); + final Directory macLibrary = + federatedPlugin.childDirectory('my_plugin_macos')..createSync(); + createFakePubspec(macLibrary); + + // Test without specifying `--type`. + final List plugins = + await runCapturingPrint(runner, ['list']); + + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + '/packages/my_plugin/my_plugin', + '/packages/my_plugin/my_plugin_web', + '/packages/my_plugin/my_plugin_macos', + ]), + ); + + cleanupPackages(); + }); + + test('can filter plugins with the --plugins argument', () async { + createFakePlugin('plugin1'); + + // Create a federated plugin by creating a directory under the packages + // directory with several packages underneath. + final Directory federatedPlugin = + mockPackagesDir.childDirectory('my_plugin')..createSync(); + final Directory clientLibrary = + federatedPlugin.childDirectory('my_plugin')..createSync(); + createFakePubspec(clientLibrary); + final Directory webLibrary = + federatedPlugin.childDirectory('my_plugin_web')..createSync(); + createFakePubspec(webLibrary); + final Directory macLibrary = + federatedPlugin.childDirectory('my_plugin_macos')..createSync(); + createFakePubspec(macLibrary); + + List plugins = await runCapturingPrint( + runner, ['list', '--plugins=plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--plugins=my_plugin']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin', + '/packages/my_plugin/my_plugin_web', + '/packages/my_plugin/my_plugin_macos', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--plugins=my_plugin/my_plugin_web']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin_web', + ]), + ); + + plugins = await runCapturingPrint(runner, + ['list', '--plugins=my_plugin/my_plugin_web,plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + '/packages/my_plugin/my_plugin_web', + ]), + ); + }); + }); +} diff --git a/packages/fluttter_plugin_tools/test/mocks.dart b/packages/fluttter_plugin_tools/test/mocks.dart new file mode 100644 index 000000000000..3e17ff8efd32 --- /dev/null +++ b/packages/fluttter_plugin_tools/test/mocks.dart @@ -0,0 +1,33 @@ +import 'dart:async'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:mockito/mockito.dart'; + +class MockProcess extends Mock implements io.Process { + final Completer exitCodeCompleter = Completer(); + final StreamController> stdoutController = + StreamController>(); + final StreamController> stderrController = + StreamController>(); + final MockIOSink stdinMock = MockIOSink(); + + @override + Future get exitCode => exitCodeCompleter.future; + + @override + Stream> get stdout => stdoutController.stream; + + @override + Stream> get stderr => stderrController.stream; + + @override + IOSink get stdin => stdinMock; +} + +class MockIOSink extends Mock implements IOSink { + List lines = []; + + @override + void writeln([Object obj = ""]) => lines.add(obj); +} diff --git a/packages/fluttter_plugin_tools/test/publish_plugin_command_test.dart b/packages/fluttter_plugin_tools/test/publish_plugin_command_test.dart new file mode 100644 index 000000000000..d01bfa67ce24 --- /dev/null +++ b/packages/fluttter_plugin_tools/test/publish_plugin_command_test.dart @@ -0,0 +1,359 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:git/git.dart'; +import 'package:matcher/matcher.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + const String testPluginName = 'foo'; + final List printedMessages = []; + + Directory parentDir; + Directory pluginDir; + GitDir gitDir; + TestProcessRunner processRunner; + CommandRunner commandRunner; + MockStdin mockStdin; + + setUp(() async { + // This test uses a local file system instead of an in memory one throughout + // so that git actually works. In setup we initialize a mono repo of plugins + // with one package and commit everything to Git. + parentDir = const LocalFileSystem() + .systemTempDirectory + .createTempSync('publish_plugin_command_test-'); + initializeFakePackages(parentDir: parentDir); + pluginDir = createFakePlugin(testPluginName, withSingleExample: false); + assert(pluginDir != null && pluginDir.existsSync()); + createFakePubspec(pluginDir, includeVersion: true); + io.Process.runSync('git', ['init'], + workingDirectory: mockPackagesDir.path); + gitDir = await GitDir.fromExisting(mockPackagesDir.path); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Initial commit']); + processRunner = TestProcessRunner(); + mockStdin = MockStdin(); + commandRunner = CommandRunner('tester', '') + ..addCommand(PublishPluginCommand( + mockPackagesDir, const LocalFileSystem(), + processRunner: processRunner, + print: (Object message) => printedMessages.add(message.toString()), + stdinput: mockStdin)); + }); + + tearDown(() { + parentDir.deleteSync(recursive: true); + printedMessages.clear(); + }); + + group('Initial validation', () { + test('requires a package flag', () async { + await expectLater(() => commandRunner.run(['publish-plugin']), + throwsA(const TypeMatcher())); + + expect( + printedMessages.last, contains("Must specify a package to publish.")); + }); + + test('requires an existing flag', () async { + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', 'iamerror']), + throwsA(const TypeMatcher())); + + expect(printedMessages.last, contains('iamerror does not exist')); + }); + + test('refuses to proceed with dirty files', () async { + pluginDir.childFile('tmp').createSync(); + + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', testPluginName]), + throwsA(const TypeMatcher())); + + expect( + printedMessages.last, + contains( + "There are files in the package directory that haven't been saved in git.")); + }); + + test('fails immediately if the remote doesn\'t exist', () async { + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', testPluginName]), + throwsA(const TypeMatcher())); + + expect(processRunner.results.last.stderr, contains("No such remote")); + }); + + test("doesn't validate the remote if it's not pushing tags", () async { + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + + expect(printedMessages.last, 'Done!'); + }); + }); + + group('Publishes package', () { + test('while showing all output from pub publish to the user', () async { + final Future publishCommand = commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + processRunner.mockPublishProcess.stdoutController.add(utf8.encode('Foo')); + processRunner.mockPublishProcess.stderrController.add(utf8.encode('Bar')); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await publishCommand; + + expect(printedMessages, contains('Foo')); + expect(printedMessages, contains('Bar')); + }); + + test('forwards input from the user to `pub publish`', () async { + final Future publishCommand = commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + mockStdin.controller.add(utf8.encode('user input')); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await publishCommand; + + expect(processRunner.mockPublishProcess.stdinMock.lines, + contains('user input')); + }); + + test('forwards --pub-publish-flags to pub publish', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + '--pub-publish-flags', + '--dry-run,--server=foo' + ]); + + expect(processRunner.mockPublishArgs.length, 4); + expect(processRunner.mockPublishArgs[0], 'pub'); + expect(processRunner.mockPublishArgs[1], 'publish'); + expect(processRunner.mockPublishArgs[2], '--dry-run'); + expect(processRunner.mockPublishArgs[3], '--server=foo'); + }); + + test('throws if pub publish fails', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(128); + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains("Publish failed. Exiting.")); + }); + }); + + group('Tags release', () { + test('with the version and name from the pubspec.yaml', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ]); + + final String tag = + (await gitDir.runCommand(['show-ref', 'fake_package-v0.0.1'])) + .stdout; + expect(tag, isNotEmpty); + }); + + test('only if publishing succeeded', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(128); + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains("Publish failed. Exiting.")); + final String tag = (await gitDir.runCommand( + ['show-ref', 'fake_package-v0.0.1'], + throwOnError: false)) + .stdout; + expect(tag, isEmpty); + }); + }); + + group('Pushes tags', () { + setUp(() async { + await gitDir.runCommand( + ['remote', 'add', 'upstream', 'http://localhost:8000']); + }); + + test('requires user confirmation', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'help'; + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains('Tag push canceled.')); + }); + + test('to upstream by default', () async { + await gitDir.runCommand(['tag', 'garbage']); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + + test('to different remotes based on a flag', () async { + await gitDir.runCommand( + ['remote', 'add', 'origin', 'http://localhost:8001']); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--remote', + 'origin', + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'origin'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + + test('only if tagging and pushing to remotes are both enabled', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-tag-release', + ]); + + expect(processRunner.pushTagsArgs.isEmpty, isTrue); + expect(printedMessages.last, 'Done!'); + }); + }); +} + +class TestProcessRunner extends ProcessRunner { + final List results = []; + final MockProcess mockPublishProcess = MockProcess(); + final List mockPublishArgs = []; + final MockProcessResult mockPushTagsResult = MockProcessResult(); + final List pushTagsArgs = []; + + @override + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + // Don't ever really push tags. + if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { + pushTagsArgs.addAll(args); + return mockPushTagsResult; + } + + final io.ProcessResult result = io.Process.runSync(executable, args, + workingDirectory: workingDir?.path); + results.add(result); + if (result.exitCode != 0) { + throw ToolExit(result.exitCode); + } + return result; + } + + @override + Future start(String executable, List args, + {Directory workingDirectory}) async { + /// Never actually publish anything. Start is always and only used for this + /// since it returns something we can route stdin through. + assert(executable == 'flutter' && + args.isNotEmpty && + args[0] == 'pub' && + args[1] == 'publish'); + mockPublishArgs.addAll(args); + return mockPublishProcess; + } +} + +class MockStdin extends Mock implements io.Stdin { + final StreamController> controller = StreamController>(); + String readLineOutput; + + @override + Stream transform(StreamTransformer streamTransformer) { + return controller.stream.transform(streamTransformer); + } + + @override + StreamSubscription> listen(void onData(List event), + {Function onError, void onDone(), bool cancelOnError}) { + return controller.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + String readLineSync( + {Encoding encoding = io.systemEncoding, + bool retainNewlines = false}) => + readLineOutput; +} + +class MockProcessResult extends Mock implements io.ProcessResult {} diff --git a/packages/fluttter_plugin_tools/test/test_command_test.dart b/packages/fluttter_plugin_tools/test/test_command_test.dart new file mode 100644 index 000000000000..514e4c27190a --- /dev/null +++ b/packages/fluttter_plugin_tools/test/test_command_test.dart @@ -0,0 +1,154 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/test_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$TestCommand', () { + CommandRunner runner; + final RecordingProcessRunner processRunner = RecordingProcessRunner(); + + setUp(() { + initializeFakePackages(); + final TestCommand command = TestCommand(mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('test_test', 'Test for $TestCommand'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + processRunner.recordedCalls.clear(); + }); + + test('runs flutter test on each plugin', () async { + final Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = + createFakePlugin('plugin2', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', ['test', '--color'], plugin1Dir.path), + ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('skips testing plugins without test directory', () async { + createFakePlugin('plugin1'); + final Directory plugin2Dir = + createFakePlugin('plugin2', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('runs pub run test on non-Flutter packages', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', + isFlutter: true, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', + isFlutter: false, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + ['test', '--color', '--enable-experiment=exp1'], + plugin1Dir.path), + ProcessCall('pub', ['get'], plugin2Dir.path), + ProcessCall( + 'pub', + ['run', '--enable-experiment=exp1', 'test'], + plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('runs on Chrome for web plugins', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ], + isFlutter: true, + isWebPlugin: true, + ); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', + ['test', '--color', '--platform=chrome'], pluginDir.path), + ]), + ); + }); + + test('enable-experiment flag', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', + isFlutter: true, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', + isFlutter: false, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + ['test', '--color', '--enable-experiment=exp1'], + plugin1Dir.path), + ProcessCall('pub', ['get'], plugin2Dir.path), + ProcessCall( + 'pub', + ['run', '--enable-experiment=exp1', 'test'], + plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + }); +} diff --git a/packages/fluttter_plugin_tools/test/util.dart b/packages/fluttter_plugin_tools/test/util.dart new file mode 100644 index 000000000000..ec0000d13f34 --- /dev/null +++ b/packages/fluttter_plugin_tools/test/util.dart @@ -0,0 +1,291 @@ +import 'dart:async'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:platform/platform.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:quiver/collection.dart'; + +FileSystem mockFileSystem = MemoryFileSystem( + style: LocalPlatform().isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix); +Directory mockPackagesDir; + +/// Creates a mock packages directory in the mock file system. +/// +/// If [parentDir] is set the mock packages dir will be creates as a child of +/// it. If not [mockFileSystem] will be used instead. +void initializeFakePackages({Directory parentDir}) { + mockPackagesDir = + (parentDir ?? mockFileSystem.currentDirectory).childDirectory('packages'); + mockPackagesDir.createSync(); +} + +/// Creates a plugin package with the given [name] in [mockPackagesDir]. +Directory createFakePlugin( + String name, { + bool withSingleExample = false, + List withExamples = const [], + List> withExtraFiles = const >[], + bool isFlutter = true, + bool isAndroidPlugin = false, + bool isIosPlugin = false, + bool isWebPlugin = false, + bool isLinuxPlugin = false, + bool isMacOsPlugin = false, + bool isWindowsPlugin = false, + String parentDirectoryName = '', +}) { + assert(!(withSingleExample && withExamples.isNotEmpty), + 'cannot pass withSingleExample and withExamples simultaneously'); + + final Directory pluginDirectory = (parentDirectoryName != '') + ? mockPackagesDir.childDirectory(parentDirectoryName).childDirectory(name) + : mockPackagesDir.childDirectory(name); + pluginDirectory.createSync(recursive: true); + + createFakePubspec( + pluginDirectory, + name: name, + isFlutter: isFlutter, + isAndroidPlugin: isAndroidPlugin, + isIosPlugin: isIosPlugin, + isWebPlugin: isWebPlugin, + isLinuxPlugin: isLinuxPlugin, + isMacOsPlugin: isMacOsPlugin, + isWindowsPlugin: isWindowsPlugin, + ); + + if (withSingleExample) { + final Directory exampleDir = pluginDirectory.childDirectory('example') + ..createSync(); + createFakePubspec(exampleDir, + name: "${name}_example", isFlutter: isFlutter); + } else if (withExamples.isNotEmpty) { + final Directory exampleDir = pluginDirectory.childDirectory('example') + ..createSync(); + for (String example in withExamples) { + final Directory currentExample = exampleDir.childDirectory(example) + ..createSync(); + createFakePubspec(currentExample, name: example, isFlutter: isFlutter); + } + } + + for (List file in withExtraFiles) { + final List newFilePath = [pluginDirectory.path] + ..addAll(file); + final File newFile = + mockFileSystem.file(mockFileSystem.path.joinAll(newFilePath)); + newFile.createSync(recursive: true); + } + + return pluginDirectory; +} + +/// Creates a `pubspec.yaml` file with a flutter dependency. +void createFakePubspec( + Directory parent, { + String name = 'fake_package', + bool isFlutter = true, + bool includeVersion = false, + bool isAndroidPlugin = false, + bool isIosPlugin = false, + bool isWebPlugin = false, + bool isLinuxPlugin = false, + bool isMacOsPlugin = false, + bool isWindowsPlugin = false, +}) { + parent.childFile('pubspec.yaml').createSync(); + String yaml = ''' +name: $name +flutter: + plugin: + platforms: +'''; + if (isAndroidPlugin) { + yaml += ''' + android: + package: io.flutter.plugins.fake + pluginClass: FakePlugin +'''; + } + if (isIosPlugin) { + yaml += ''' + ios: + pluginClass: FLTFakePlugin +'''; + } + if (isWebPlugin) { + yaml += ''' + web: + pluginClass: FakePlugin + fileName: ${name}_web.dart +'''; + } + if (isLinuxPlugin) { + yaml += ''' + linux: + pluginClass: FakePlugin +'''; + } + if (isMacOsPlugin) { + yaml += ''' + macos: + pluginClass: FakePlugin +'''; + } + if (isWindowsPlugin) { + yaml += ''' + windows: + pluginClass: FakePlugin +'''; + } + if (isFlutter) { + yaml += ''' +dependencies: + flutter: + sdk: flutter +'''; + } + if (includeVersion) { + yaml += ''' +version: 0.0.1 +publish_to: none # Hardcoded safeguard to prevent this from somehow being published by a broken test. +'''; + } + parent.childFile('pubspec.yaml').writeAsStringSync(yaml); +} + +/// Cleans up the mock packages directory, making it an empty directory again. +void cleanupPackages() { + mockPackagesDir.listSync().forEach((FileSystemEntity entity) { + entity.deleteSync(recursive: true); + }); +} + +/// Run the command [runner] with the given [args] and return +/// what was printed. +Future> runCapturingPrint( + CommandRunner runner, List args) async { + final List prints = []; + final ZoneSpecification spec = ZoneSpecification( + print: (_, __, ___, String message) { + prints.add(message); + }, + ); + await Zone.current + .fork(specification: spec) + .run>(() => runner.run(args)); + + return prints; +} + +/// A mock [ProcessRunner] which records process calls. +class RecordingProcessRunner extends ProcessRunner { + io.Process processToReturn; + final List recordedCalls = []; + + /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. + String resultStdout; + + /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. + String resultStderr; + + @override + Future runAndStream( + String executable, + List args, { + Directory workingDir, + bool exitOnError = false, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + return Future.value( + processToReturn == null ? 0 : await processToReturn.exitCode); + } + + /// Returns [io.ProcessResult] created from [processToReturn], [resultStdout], and [resultStderr]. + @override + Future run(String executable, List args, + {Directory workingDir, + bool exitOnError = false, + stdoutEncoding = io.systemEncoding, + stderrEncoding = io.systemEncoding}) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + io.ProcessResult result; + + if (processToReturn != null) { + result = io.ProcessResult( + processToReturn.pid, + await processToReturn.exitCode, + resultStdout ?? processToReturn.stdout, + resultStderr ?? processToReturn.stderr); + } + return Future.value(result); + } + + @override + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + io.ProcessResult result; + if (processToReturn != null) { + result = io.ProcessResult( + processToReturn.pid, + await processToReturn.exitCode, + resultStdout ?? processToReturn.stdout, + resultStderr ?? processToReturn.stderr); + } + return Future.value(result); + } + + @override + Future start(String executable, List args, + {Directory workingDirectory}) async { + recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); + return Future.value(processToReturn); + } +} + +/// A recorded process call. +class ProcessCall { + const ProcessCall(this.executable, this.args, this.workingDir); + + /// The executable that was called. + final String executable; + + /// The arguments passed to [executable] in the call. + final List args; + + /// The working directory this process was called from. + final String workingDir; + + @override + bool operator ==(dynamic other) { + if (other is! ProcessCall) { + return false; + } + final ProcessCall otherCall = other; + return executable == otherCall.executable && + listsEqual(args, otherCall.args) && + workingDir == otherCall.workingDir; + } + + @override + int get hashCode => + executable?.hashCode ?? + 0 ^ args?.hashCode ?? + 0 ^ workingDir?.hashCode ?? + 0; + + @override + String toString() { + final List command = [executable]..addAll(args); + return '"${command.join(' ')}" in $workingDir'; + } +} diff --git a/packages/fluttter_plugin_tools/test/version_check_test.dart b/packages/fluttter_plugin_tools/test/version_check_test.dart new file mode 100644 index 000000000000..b9ace3811bff --- /dev/null +++ b/packages/fluttter_plugin_tools/test/version_check_test.dart @@ -0,0 +1,319 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:git/git.dart'; +import 'package:mockito/mockito.dart'; +import "package:test/test.dart"; +import "package:flutter_plugin_tools/src/version_check_command.dart"; +import 'package:pub_semver/pub_semver.dart'; +import 'util.dart'; + +void testAllowedVersion( + String masterVersion, + String headVersion, { + bool allowed = true, + NextVersionType nextVersionType, +}) { + final Version master = Version.parse(masterVersion); + final Version head = Version.parse(headVersion); + final Map allowedVersions = + getAllowedNextVersions(master, head); + if (allowed) { + expect(allowedVersions, contains(head)); + if (nextVersionType != null) { + expect(allowedVersions[head], equals(nextVersionType)); + } + } else { + expect(allowedVersions, isNot(contains(head))); + } +} + +class MockGitDir extends Mock implements GitDir {} + +class MockProcessResult extends Mock implements ProcessResult {} + +void main() { + group('$VersionCheckCommand', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + List> gitDirCommands; + String gitDiffResponse; + Map gitShowResponses; + + setUp(() { + gitDirCommands = >[]; + gitDiffResponse = ''; + gitShowResponses = {}; + final MockGitDir gitDir = MockGitDir(); + when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { + gitDirCommands.add(invocation.positionalArguments[0]); + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'diff') { + when(mockProcessResult.stdout).thenReturn(gitDiffResponse); + } else if (invocation.positionalArguments[0][0] == 'show') { + final String response = + gitShowResponses[invocation.positionalArguments[0][1]]; + when(mockProcessResult.stdout).thenReturn(response); + } + return Future.value(mockProcessResult); + }); + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final VersionCheckCommand command = VersionCheckCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, gitDir: gitDir); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('allows valid version', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect(gitDirCommands[1].join(' '), + equals('show master:packages/plugin/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin/pubspec.yaml')); + }); + + test('denies invalid version', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', + }; + final Future> result = runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect(gitDirCommands[1].join(' '), + equals('show master:packages/plugin/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin/pubspec.yaml')); + }); + + test('gracefully handles missing pubspec.yaml', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + mockFileSystem.currentDirectory + .childDirectory('packages') + .childDirectory('plugin') + .childFile('pubspec.yaml') + .deleteSync(); + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(1)); + expect(gitDirCommands.first.join(' '), + equals('diff --name-only master HEAD')); + }); + + test('allows minor changes to platform interfaces', () async { + createFakePlugin('plugin_platform_interface'); + gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.1.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect( + gitDirCommands[1].join(' '), + equals( + 'show master:packages/plugin_platform_interface/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); + }); + + test('disallows breaking changes to platform interfaces', () async { + createFakePlugin('plugin_platform_interface'); + gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml': + 'version: 2.0.0', + }; + final Future> output = runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + await expectLater( + output, + throwsA(const TypeMatcher()), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect( + gitDirCommands[1].join(' '), + equals( + 'show master:packages/plugin_platform_interface/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); + }); + }); + + group("Pre 1.0", () { + test("nextVersion allows patch version", () { + testAllowedVersion("0.12.0", "0.12.0+1", + nextVersionType: NextVersionType.PATCH); + testAllowedVersion("0.12.0+4", "0.12.0+5", + nextVersionType: NextVersionType.PATCH); + }); + + test("nextVersion does not allow jumping patch", () { + testAllowedVersion("0.12.0", "0.12.0+2", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.0+4", allowed: false); + }); + + test("nextVersion does not allow going back", () { + testAllowedVersion("0.12.0", "0.11.0", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.0+1", allowed: false); + testAllowedVersion("0.12.0+1", "0.12.0", allowed: false); + }); + + test("nextVersion allows minor version", () { + testAllowedVersion("0.12.0", "0.12.1", + nextVersionType: NextVersionType.MINOR); + testAllowedVersion("0.12.0+4", "0.12.1", + nextVersionType: NextVersionType.MINOR); + }); + + test("nextVersion does not allow jumping minor", () { + testAllowedVersion("0.12.0", "0.12.2", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.3", allowed: false); + }); + }); + + group("Releasing 1.0", () { + test("nextVersion allows releasing 1.0", () { + testAllowedVersion("0.12.0", "1.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion("0.12.0+4", "1.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test("nextVersion does not allow jumping major", () { + testAllowedVersion("0.12.0", "2.0.0", allowed: false); + testAllowedVersion("0.12.0+4", "2.0.0", allowed: false); + }); + + test("nextVersion does not allow un-releasing", () { + testAllowedVersion("1.0.0", "0.12.0+4", allowed: false); + testAllowedVersion("1.0.0", "0.12.0", allowed: false); + }); + }); + + group("Post 1.0", () { + test("nextVersion allows patch jumps", () { + testAllowedVersion("1.0.1", "1.0.2", + nextVersionType: NextVersionType.PATCH); + testAllowedVersion("1.0.0", "1.0.1", + nextVersionType: NextVersionType.PATCH); + }); + + test("nextVersion does not allow build jumps", () { + testAllowedVersion("1.0.1", "1.0.1+1", allowed: false); + testAllowedVersion("1.0.0+5", "1.0.0+6", allowed: false); + }); + + test("nextVersion does not allow skipping patches", () { + testAllowedVersion("1.0.1", "1.0.3", allowed: false); + testAllowedVersion("1.0.0", "1.0.6", allowed: false); + }); + + test("nextVersion allows minor version jumps", () { + testAllowedVersion("1.0.1", "1.1.0", + nextVersionType: NextVersionType.MINOR); + testAllowedVersion("1.0.0", "1.1.0", + nextVersionType: NextVersionType.MINOR); + }); + + test("nextVersion does not allow skipping minor versions", () { + testAllowedVersion("1.0.1", "1.2.0", allowed: false); + testAllowedVersion("1.1.0", "1.3.0", allowed: false); + }); + + test("nextVersion allows breaking changes", () { + testAllowedVersion("1.0.1", "2.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion("1.0.0", "2.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test("nextVersion allows null safety pre prelease", () { + testAllowedVersion("1.0.1", "2.0.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0", "2.0.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0-nullsafety", "1.0.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0-nullsafety.1", "1.0.0-nullsafety.2", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0", "0.2.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0-nullsafety", "0.1.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0-nullsafety.1", "0.1.0-nullsafety.2", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0", "1.1.0-nullsafety", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.1.0-nullsafety", "1.1.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0", "0.1.1-nullsafety", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.1-nullsafety", "0.1.1-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + }); + + test("nextVersion does not allow skipping major versions", () { + testAllowedVersion("1.0.1", "3.0.0", allowed: false); + testAllowedVersion("1.1.0", "2.3.0", allowed: false); + }); + }); +} diff --git a/packages/fluttter_plugin_tools/test/xctest_command_test.dart b/packages/fluttter_plugin_tools/test/xctest_command_test.dart new file mode 100644 index 000000000000..007c2e12188c --- /dev/null +++ b/packages/fluttter_plugin_tools/test/xctest_command_test.dart @@ -0,0 +1,358 @@ +// 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 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/xctest_command.dart'; +import 'package:test/test.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +final _kDeviceListMap = { + "runtimes": [ + { + "bundlePath": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime", + "buildversion": "17A577", + "runtimeRoot": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-0", + "version": "13.0", + "isAvailable": true, + "name": "iOS 13.0" + }, + { + "bundlePath": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime", + "buildversion": "17L255", + "runtimeRoot": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-4", + "version": "13.4", + "isAvailable": true, + "name": "iOS 13.4" + }, + { + "bundlePath": + "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime", + "buildversion": "17T531", + "runtimeRoot": + "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.watchOS-6-2", + "version": "6.2.1", + "isAvailable": true, + "name": "watchOS 6.2" + } + ], + "devices": { + "com.apple.CoreSimulator.SimRuntime.iOS-13-4": [ + { + "dataPath": + "/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data", + "logPath": + "/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774", + "udid": "2706BBEB-1E01-403E-A8E9-70E8E5A24774", + "isAvailable": true, + "deviceTypeIdentifier": + "com.apple.CoreSimulator.SimDeviceType.iPhone-8", + "state": "Shutdown", + "name": "iPhone 8" + }, + { + "dataPath": + "/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data", + "logPath": + "/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A", + "udid": "1E76A0FD-38AC-4537-A989-EA639D7D012A", + "isAvailable": true, + "deviceTypeIdentifier": + "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus", + "state": "Shutdown", + "name": "iPhone 8 Plus" + } + ] + } +}; + +void main() { + const String _kDestination = '--ios-destination'; + const String _kTarget = '--target'; + const String _kSkip = '--skip'; + + group('test xctest_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final XCTestCommand command = XCTestCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('xctest_command', 'Test for xctest_command'); + runner.addCommand(command); + cleanupPackages(); + }); + + test('Not specifying --target throws', () async { + await expectLater( + () => runner.run(['xctest', _kDestination, 'a_destination']), + throwsA(const TypeMatcher())); + }); + + test('skip if ios is not supported', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + expect(output, contains('iOS is not supported by this plugin.')); + expect(processRunner.recordedCalls, orderedEquals([])); + + cleanupPackages(); + }); + + test('running with correct scheme and destination, did not find scheme', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = '{"project":{"targets":["bar_scheme"]}}'; + + await expectLater(() async { + final List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + expect(output, + contains('foo_scheme not configured for plugin, test failed.')); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('xcrun', ['simctl', 'list', '--json'], null), + ProcessCall( + 'xcodebuild', + [ + '-project', + 'ios/Runner.xcodeproj', + '-list', + '-json' + ], + pluginExampleDirectory.path), + ])); + }, throwsA(const TypeMatcher())); + cleanupPackages(); + }); + + test('running with correct scheme and destination, found scheme', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + + expect(output, contains('Successfully ran xctest for plugin')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'foo_destination', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory.path), + ])); + + cleanupPackages(); + }); + + test('running with correct scheme and destination, skip 1 plugin', + () async { + createFakePlugin('plugin1', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + createFakePlugin('plugin2', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory1 = + mockPackagesDir.childDirectory('plugin1').childDirectory('example'); + createFakePubspec(pluginExampleDirectory1, isFlutter: true); + final Directory pluginExampleDirectory2 = + mockPackagesDir.childDirectory('plugin2').childDirectory('example'); + createFakePubspec(pluginExampleDirectory2, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination', + _kSkip, + 'plugin1' + ]); + + expect(output, contains('plugin1 was skipped with the --skip flag.')); + expect(output, contains('Successfully ran xctest for plugin2')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory2.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'foo_destination', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory2.path), + ])); + + cleanupPackages(); + }); + + test('Not specifying --ios-destination assigns an available simulator', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final Map schemeCommandResult = { + "project": { + "targets": ["bar_scheme", "foo_scheme"] + } + }; + // For simplicity of the test, we combine all the mock results into a single mock result, each internal command + // will get this result and they should still be able to parse them correctly. + processRunner.resultStdout = + jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); + await runner.run([ + 'xctest', + _kTarget, + 'foo_scheme', + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('xcrun', ['simctl', 'list', '--json'], null), + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory.path), + ])); + + cleanupPackages(); + }); + }); +} From bed1a635f98f39c3a6f8ced999c9411441f2fc06 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 17:26:52 -0800 Subject: [PATCH 15/27] remove plugin_tools --- .cirrus.yml | 2 +- packages/fluttter_plugin_tools/CHANGELOG.md | 288 ---------- packages/fluttter_plugin_tools/LICENSE | 29 - packages/fluttter_plugin_tools/README.md | 45 -- .../analysis_options.yaml | 1 - .../bin/flutter_plugin_tools.dart | 5 - .../lib/src/analyze_command.dart | 94 ---- .../lib/src/build_examples_command.dart | 191 ------- .../fluttter_plugin_tools/lib/src/common.dart | 466 ---------------- .../src/create_all_plugins_app_command.dart | 200 ------- .../lib/src/drive_examples_command.dart | 210 -------- .../lib/src/firebase_test_lab_command.dart | 264 --------- .../lib/src/format_command.dart | 147 ----- .../lib/src/java_test_command.dart | 89 --- .../lib/src/lint_podspecs_command.dart | 146 ----- .../lib/src/list_command.dart | 60 --- .../fluttter_plugin_tools/lib/src/main.dart | 63 --- .../lib/src/publish_plugin_command.dart | 227 -------- .../lib/src/test_command.dart | 101 ---- .../lib/src/version_check_command.dart | 220 -------- .../lib/src/xctest_command.dart | 216 -------- packages/fluttter_plugin_tools/pubspec.yaml | 30 -- .../test/analyze_command_test.dart | 93 ---- .../test/build_examples_command_test.dart | 470 ---------------- .../test/common_test.dart | 100 ---- .../test/drive_examples_command_test.dart | 505 ------------------ .../test/firebase_test_lab_test.dart | 256 --------- .../test/lint_podspecs_command_test.dart | 202 ------- .../test/list_command_test.dart | 198 ------- .../fluttter_plugin_tools/test/mocks.dart | 33 -- .../test/publish_plugin_command_test.dart | 359 ------------- .../test/test_command_test.dart | 154 ------ packages/fluttter_plugin_tools/test/util.dart | 291 ---------- .../test/version_check_test.dart | 319 ----------- .../test/xctest_command_test.dart | 358 ------------- packages/plugin_tools/CHANGELOG.md | 288 ---------- packages/plugin_tools/LICENSE | 29 - packages/plugin_tools/README.md | 45 -- packages/plugin_tools/analysis_options.yaml | 1 - .../bin/flutter_plugin_tools.dart | 5 - .../plugin_tools/lib/src/analyze_command.dart | 94 ---- .../lib/src/build_examples_command.dart | 191 ------- packages/plugin_tools/lib/src/common.dart | 466 ---------------- .../src/create_all_plugins_app_command.dart | 200 ------- .../lib/src/drive_examples_command.dart | 210 -------- .../lib/src/firebase_test_lab_command.dart | 264 --------- .../plugin_tools/lib/src/format_command.dart | 147 ----- .../lib/src/java_test_command.dart | 89 --- .../lib/src/lint_podspecs_command.dart | 146 ----- .../plugin_tools/lib/src/list_command.dart | 60 --- packages/plugin_tools/lib/src/main.dart | 63 --- .../lib/src/publish_plugin_command.dart | 227 -------- .../plugin_tools/lib/src/test_command.dart | 101 ---- .../lib/src/version_check_command.dart | 220 -------- .../plugin_tools/lib/src/xctest_command.dart | 216 -------- packages/plugin_tools/pubspec.yaml | 30 -- .../test/analyze_command_test.dart | 93 ---- .../test/build_examples_command_test.dart | 470 ---------------- packages/plugin_tools/test/common_test.dart | 100 ---- .../test/drive_examples_command_test.dart | 505 ------------------ .../test/firebase_test_lab_test.dart | 256 --------- .../test/lint_podspecs_command_test.dart | 202 ------- .../plugin_tools/test/list_command_test.dart | 198 ------- packages/plugin_tools/test/mocks.dart | 33 -- .../test/publish_plugin_command_test.dart | 359 ------------- .../plugin_tools/test/test_command_test.dart | 154 ------ packages/plugin_tools/test/util.dart | 291 ---------- .../plugin_tools/test/version_check_test.dart | 319 ----------- .../test/xctest_command_test.dart | 358 ------------- 69 files changed, 1 insertion(+), 12861 deletions(-) delete mode 100644 packages/fluttter_plugin_tools/CHANGELOG.md delete mode 100644 packages/fluttter_plugin_tools/LICENSE delete mode 100644 packages/fluttter_plugin_tools/README.md delete mode 100644 packages/fluttter_plugin_tools/analysis_options.yaml delete mode 100644 packages/fluttter_plugin_tools/bin/flutter_plugin_tools.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/analyze_command.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/build_examples_command.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/common.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/create_all_plugins_app_command.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/drive_examples_command.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/firebase_test_lab_command.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/format_command.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/java_test_command.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/lint_podspecs_command.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/list_command.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/main.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/publish_plugin_command.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/test_command.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/version_check_command.dart delete mode 100644 packages/fluttter_plugin_tools/lib/src/xctest_command.dart delete mode 100644 packages/fluttter_plugin_tools/pubspec.yaml delete mode 100644 packages/fluttter_plugin_tools/test/analyze_command_test.dart delete mode 100644 packages/fluttter_plugin_tools/test/build_examples_command_test.dart delete mode 100644 packages/fluttter_plugin_tools/test/common_test.dart delete mode 100644 packages/fluttter_plugin_tools/test/drive_examples_command_test.dart delete mode 100644 packages/fluttter_plugin_tools/test/firebase_test_lab_test.dart delete mode 100644 packages/fluttter_plugin_tools/test/lint_podspecs_command_test.dart delete mode 100644 packages/fluttter_plugin_tools/test/list_command_test.dart delete mode 100644 packages/fluttter_plugin_tools/test/mocks.dart delete mode 100644 packages/fluttter_plugin_tools/test/publish_plugin_command_test.dart delete mode 100644 packages/fluttter_plugin_tools/test/test_command_test.dart delete mode 100644 packages/fluttter_plugin_tools/test/util.dart delete mode 100644 packages/fluttter_plugin_tools/test/version_check_test.dart delete mode 100644 packages/fluttter_plugin_tools/test/xctest_command_test.dart delete mode 100644 packages/plugin_tools/CHANGELOG.md delete mode 100644 packages/plugin_tools/LICENSE delete mode 100644 packages/plugin_tools/README.md delete mode 100644 packages/plugin_tools/analysis_options.yaml delete mode 100644 packages/plugin_tools/bin/flutter_plugin_tools.dart delete mode 100644 packages/plugin_tools/lib/src/analyze_command.dart delete mode 100644 packages/plugin_tools/lib/src/build_examples_command.dart delete mode 100644 packages/plugin_tools/lib/src/common.dart delete mode 100644 packages/plugin_tools/lib/src/create_all_plugins_app_command.dart delete mode 100644 packages/plugin_tools/lib/src/drive_examples_command.dart delete mode 100644 packages/plugin_tools/lib/src/firebase_test_lab_command.dart delete mode 100644 packages/plugin_tools/lib/src/format_command.dart delete mode 100644 packages/plugin_tools/lib/src/java_test_command.dart delete mode 100644 packages/plugin_tools/lib/src/lint_podspecs_command.dart delete mode 100644 packages/plugin_tools/lib/src/list_command.dart delete mode 100644 packages/plugin_tools/lib/src/main.dart delete mode 100644 packages/plugin_tools/lib/src/publish_plugin_command.dart delete mode 100644 packages/plugin_tools/lib/src/test_command.dart delete mode 100644 packages/plugin_tools/lib/src/version_check_command.dart delete mode 100644 packages/plugin_tools/lib/src/xctest_command.dart delete mode 100644 packages/plugin_tools/pubspec.yaml delete mode 100644 packages/plugin_tools/test/analyze_command_test.dart delete mode 100644 packages/plugin_tools/test/build_examples_command_test.dart delete mode 100644 packages/plugin_tools/test/common_test.dart delete mode 100644 packages/plugin_tools/test/drive_examples_command_test.dart delete mode 100644 packages/plugin_tools/test/firebase_test_lab_test.dart delete mode 100644 packages/plugin_tools/test/lint_podspecs_command_test.dart delete mode 100644 packages/plugin_tools/test/list_command_test.dart delete mode 100644 packages/plugin_tools/test/mocks.dart delete mode 100644 packages/plugin_tools/test/publish_plugin_command_test.dart delete mode 100644 packages/plugin_tools/test/test_command_test.dart delete mode 100644 packages/plugin_tools/test/util.dart delete mode 100644 packages/plugin_tools/test/version_check_test.dart delete mode 100644 packages/plugin_tools/test/xctest_command_test.dart diff --git a/.cirrus.yml b/.cirrus.yml index cba4c743b414..c6a199b0f260 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -146,7 +146,7 @@ task: - flutter channel master - flutter upgrade - git fetch origin master - activate_script: pub global activate -s path ./packages/plugin_tools + activate_script: pub global activate -s path ./packages/flutter_plugin_tools create_simulator_script: - xcrun simctl list - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-3 | xargs xcrun simctl boot diff --git a/packages/fluttter_plugin_tools/CHANGELOG.md b/packages/fluttter_plugin_tools/CHANGELOG.md deleted file mode 100644 index 25dce424eee4..000000000000 --- a/packages/fluttter_plugin_tools/CHANGELOG.md +++ /dev/null @@ -1,288 +0,0 @@ -## v.0.0.45+1 - -- Don't call `flutter format` if there are no Dart files to format. - -## v.0.0.45 - -- Add exclude flag to exclude any plugin from further processing. - -## v.0.0.44+7 - -- `all-plugins-app` doesn't override the AGP version. - -## v.0.0.44+6 - -- Fix code formatting. - -## v.0.0.44+5 - -- Remove `-v` flag on drive-examples. - -## v.0.0.44+4 - -- Fix bug where directory isn't passed - -## v.0.0.44+3 - -- More verbose logging - -## v.0.0.44+2 - -- Remove pre-alpha Windows workaround to create examples on the fly. - -## v.0.0.44+1 - -- Print packages that passed tests in `xctest` command. -- Remove printing the whole list of simulators. - -## v.0.0.44 - -- Add 'xctest' command to run xctests. - -## v.0.0.43 - -- Allow minor `*-nullsafety` pre release packages. - -## v.0.0.42+1 - -- Fix test command when `--enable-experiment` is called. - -## v.0.0.42 - -- Allow `*-nullsafety` pre release packages. - -## v.0.0.41 - -- Support `--enable-experiment` flag in subcommands `test`, `build-examples`, `drive-examples`, -and `firebase-test-lab`. - -## v.0.0.40 - -- Support `integration_test/` directory for `drive-examples` command - -## v.0.0.39 - -- Support `integration_test/` directory for `package:integration_test` - -## v.0.0.38 - -- Add C++ and ObjC++ to clang-format. - -## v.0.0.37+2 - -- Make `http` and `http_multi_server` dependency version constraint more flexible. - -## v.0.0.37+1 - -- All_plugin test puts the plugin dependencies into dependency_overrides. - -## v.0.0.37 - -- Only builds mobile example apps when necessary. - -## v.0.0.36+3 - -- Add support for Linux plugins. - -## v.0.0.36+2 - -- Default to showing podspec lint warnings - -## v.0.0.36+1 - -- Serialize linting podspecs. - -## v.0.0.36 - -- Remove retry on Firebase Test Lab's call to gcloud set. -- Remove quiet flag from Firebase Test Lab's gcloud set command. -- Allow Firebase Test Lab command to continue past gcloud set network failures. - This is a mitigation for the network service sometimes not responding, - but it isn't actually necessary to have a network connection for this command. - -## v.0.0.35+1 - -- Minor cleanup to the analyze test. - -## v.0.0.35 - -- Firebase Test Lab command generates a configurable unique path suffix for results. - -## v.0.0.34 - -- Firebase Test Lab command now only tries to configure the project once -- Firebase Test Lab command now retries project configuration up to five times. - -## v.0.0.33+1 - -- Fixes formatting issues that got past our CI due to - https://github.com/flutter/flutter/issues/51585. -- Changes the default package name for testing method `createFakePubspec` back - its previous behavior. - -## v.0.0.33 - -- Version check command now fails on breaking changes to platform interfaces. -- Updated version check test to be more flexible. - -## v.0.0.32+7 - -- Ensure that Firebase Test Lab tests have a unique storage bucket for each test run. - -## v.0.0.32+6 - -- Ensure that Firebase Test Lab tests have a unique storage bucket for each package. - -## v.0.0.32+5 - -- Remove --fail-fast and --silent from lint podspec command. - -## v.0.0.32+4 - -- Update `publish-plugin` to use `flutter pub publish` instead of just `pub - publish`. Enforces a `pub publish` command that matches the Dart SDK in the - user's Flutter install. - -## v.0.0.32+3 - -- Update Firebase Testlab deprecated test device. (Pixel 3 API 28 -> Pixel 4 API 29). - -## v.0.0.32+2 - -- Runs pub get before building macos to avoid failures. - -## v.0.0.32+1 - -- Default macOS example builds to false. Previously they were running whenever - CI was itself running on macOS. - -## v.0.0.32 - -- `analyze` now asserts that the global `analysis_options.yaml` is the only one - by default. Individual directories can be excluded from this check with the - new `--custom-analysis` flag. - -## v.0.0.31+1 - -- Add --skip and --no-analyze flags to podspec command. - -## v.0.0.31 - -- Add support for macos on `DriveExamplesCommand` and `BuildExamplesCommand`. - -## v.0.0.30 - -- Adopt pedantic analysis options, fix firebase_test_lab_test. - -## v.0.0.29 - -- Add a command to run pod lib lint on podspec files. - -## v.0.0.28 - -- Increase Firebase test lab timeouts to 5 minutes. - -## v.0.0.27 - -- Run tests with `--platform=chrome` for web plugins. - -## v.0.0.26 - -- Add a command for publishing plugins to pub. - -## v.0.0.25 - -- Update `DriveExamplesCommand` to use `ProcessRunner`. -- Make `DriveExamplesCommand` rely on `ProcessRunner` to determine if the test fails or not. -- Add simple tests for `DriveExamplesCommand`. - -## v.0.0.24 - -- Gracefully handle pubspec.yaml files for new plugins. -- Additional unit testing. - -## v.0.0.23 - -- Add a test case for transitive dependency solving in the - `create_all_plugins_app` command. - -## v.0.0.22 - -- Updated firebase-test-lab command with updated conventions for test locations. -- Updated firebase-test-lab to add an optional "device" argument. -- Updated version-check command to always compare refs instead of using the working copy. -- Added unit tests for the firebase-test-lab and version-check commands. -- Add ProcessRunner to mock running processes for testing. - -## v.0.0.21 - -- Support the `--plugins` argument for federated plugins. - -## v.0.0.20 - -- Support for finding federated plugins, where one directory contains - multiple packages for different platform implementations. - -## v.0.0.19+3 - -- Use `package:file` for file I/O. - -## v.0.0.19+2 - -- Use java as language when calling `flutter create`. - -## v.0.0.19+1 - -- Rename command for `CreateAllPluginsAppCommand`. - -## v.0.0.19 - -- Use flutter create to build app testing plugin compilation. - -## v.0.0.18+2 - -- Fix `.travis.yml` file name in `README.md`. - -## v0.0.18+1 - -- Skip version check if it contains `publish_to: none`. - -## v0.0.18 - -- Add option to exclude packages from generated pubspec command. - -## v0.0.17+4 - -- Avoid trying to version-check pubspecs that are missing a version. - -## v0.0.17+3 - -- version-check accounts for [pre-1.0 patch versions](https://github.com/flutter/flutter/issues/35412). - -## v0.0.17+2 - -- Fix exception handling for version checker - -## v0.0.17+1 - -- Fix bug where we used a flag instead of an option - -## v0.0.17 - -- Add a command for checking the version number - -## v0.0.16 - -- Add a command for generating `pubspec.yaml` for All Plugins app. - -## v0.0.15 - -- Add a command for running driver tests of plugin examples. - -## v0.0.14 - -- Check for dependencies->flutter instead of top level flutter node. - -## v0.0.13 - -- Differentiate between Flutter and non-Flutter (but potentially Flutter consumed) Dart packages. diff --git a/packages/fluttter_plugin_tools/LICENSE b/packages/fluttter_plugin_tools/LICENSE deleted file mode 100644 index 5f015bc7b321..000000000000 --- a/packages/fluttter_plugin_tools/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2017, Flutter -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/fluttter_plugin_tools/README.md b/packages/fluttter_plugin_tools/README.md deleted file mode 100644 index efdc668e81a0..000000000000 --- a/packages/fluttter_plugin_tools/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Flutter Plugin Tools - -[![Build Status](https://travis-ci.org/flutter/plugin_tools.svg?branch=master)](https://travis-ci.org/flutter/plugin_tools) -[![pub package](https://img.shields.io/pub/v/flutter_plugin_tools.svg)](https://pub.dartlang.org/packages/flutter_plugin_tools) - - -Flutter Plugin Tools implements a CLI with various productivity tools for hosting multiple Flutter plugins in one github -repository. It is mainly used by the [flutter/plugins](https://github.com/flutter/plugins) and -[flutter/flutterfire](https://github.com/flutter/flutterfire) repositories. It was mainly written to facilitate -testing on Travis for these repositories (see [.travis.yml](https://github.com/flutter/plugins/blob/master/.travis.yml)). - -As an example, Flutter Plugin Tools allows you to: - -* Build all plugin example apps with one command -* Run the tests of all plugins with one command -* Format all Dart, Java, Objective-C, and C++ code in the repository -* Define shards of the above tasks - -## Installation - -In order to use the tools you need to enable them once by running the following command: - -```shell -$ pub global activate flutter_plugin_tools -``` - -## Requirements - -To use all features of `flutter_plugin_tools` you'll need the following commands in your `PATH`: -* `flutter` -* `git` -* `pub` (recommended: version from `/bin/cache/dart-sdk/bin`) -* `clang-format` version 5 (alternatively, you can provide the path via `--clang-format=`) -* [`pod`](https://guides.cocoapods.org/using/getting-started.html#installation) (macOS only) - -## Usage - -```shell -$ pub global run flutter_plugin_tools -$ pub global run flutter_plugin_tools --shardIndex 0 --shardCount 3 -``` - -Run commands from the `flutter/plugins` directory. Replace `` with `help` to print a list of available commands. -The sharded example above divides the plugins into three shards -and executes the tool on the first shard (index 0). diff --git a/packages/fluttter_plugin_tools/analysis_options.yaml b/packages/fluttter_plugin_tools/analysis_options.yaml deleted file mode 100644 index 84a5e26f95de..000000000000 --- a/packages/fluttter_plugin_tools/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: package:pedantic/analysis_options.1.8.0.yaml diff --git a/packages/fluttter_plugin_tools/bin/flutter_plugin_tools.dart b/packages/fluttter_plugin_tools/bin/flutter_plugin_tools.dart deleted file mode 100644 index 43edcbfe2c68..000000000000 --- a/packages/fluttter_plugin_tools/bin/flutter_plugin_tools.dart +++ /dev/null @@ -1,5 +0,0 @@ -// 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. - -export 'package:flutter_plugin_tools/src/main.dart'; diff --git a/packages/fluttter_plugin_tools/lib/src/analyze_command.dart b/packages/fluttter_plugin_tools/lib/src/analyze_command.dart deleted file mode 100644 index 8cd57fa0b338..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/analyze_command.dart +++ /dev/null @@ -1,94 +0,0 @@ -// 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:file/file.dart'; -import 'package:path/path.dart' as p; - -import 'common.dart'; - -class AnalyzeCommand extends PluginCommand { - AnalyzeCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addMultiOption(_customAnalysisFlag, - help: - 'Directories (comma seperated) that are allowed to have their own analysis options.', - defaultsTo: []); - } - - static const String _customAnalysisFlag = 'custom-analysis'; - - @override - final String name = 'analyze'; - - @override - final String description = 'Analyzes all packages using package:tuneup.\n\n' - 'This command requires "pub" and "flutter" to be in your path.'; - - @override - Future run() async { - checkSharding(); - - print('Verifying analysis settings...'); - final List files = packagesDir.listSync(recursive: true); - for (final FileSystemEntity file in files) { - if (file.basename != 'analysis_options.yaml' && - file.basename != '.analysis_options') { - continue; - } - - final bool whitelisted = argResults[_customAnalysisFlag].any( - (String directory) => - p.isWithin(p.join(packagesDir.path, directory), file.path)); - if (whitelisted) { - continue; - } - - print('Found an extra analysis_options.yaml in ${file.absolute.path}.'); - print( - 'If this was deliberate, pass the package to the analyze command with the --$_customAnalysisFlag flag and try again.'); - throw ToolExit(1); - } - - print('Activating tuneup package...'); - await processRunner.runAndStream( - 'pub', ['global', 'activate', 'tuneup'], - workingDir: packagesDir, exitOnError: true); - - await for (Directory package in getPackages()) { - if (isFlutterPackage(package, fileSystem)) { - await processRunner.runAndStream('flutter', ['packages', 'get'], - workingDir: package, exitOnError: true); - } else { - await processRunner.runAndStream('pub', ['get'], - workingDir: package, exitOnError: true); - } - } - - final List failingPackages = []; - await for (Directory package in getPlugins()) { - final int exitCode = await processRunner.runAndStream( - 'pub', ['global', 'run', 'tuneup', 'check'], - workingDir: package); - if (exitCode != 0) { - failingPackages.add(p.basename(package.path)); - } - } - - print('\n\n'); - if (failingPackages.isNotEmpty) { - print('The following packages have analyzer errors (see above):'); - failingPackages.forEach((String package) { - print(' * $package'); - }); - throw ToolExit(1); - } - - print('No analyzer errors found!'); - } -} diff --git a/packages/fluttter_plugin_tools/lib/src/build_examples_command.dart b/packages/fluttter_plugin_tools/lib/src/build_examples_command.dart deleted file mode 100644 index 1493b3cce05d..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/build_examples_command.dart +++ /dev/null @@ -1,191 +0,0 @@ -// 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 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; - -import 'common.dart'; - -class BuildExamplesCommand extends PluginCommand { - BuildExamplesCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addFlag(kLinux, defaultsTo: false); - argParser.addFlag(kMacos, defaultsTo: false); - argParser.addFlag(kWindows, defaultsTo: false); - argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS); - argParser.addFlag(kApk); - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: 'Enables the given Dart SDK experiments.', - ); - } - - @override - final String name = 'build-examples'; - - @override - final String description = - 'Builds all example apps (IPA for iOS and APK for Android).\n\n' - 'This command requires "flutter" to be in your path.'; - - @override - Future run() async { - if (!argResults[kIpa] && - !argResults[kApk] && - !argResults[kLinux] && - !argResults[kMacos] && - !argResults[kWindows]) { - print( - 'None of --linux, --macos, --windows, --apk nor --ipa were specified, ' - 'so not building anything.'); - return; - } - final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - - final String enableExperiment = argResults[kEnableExperiment]; - - checkSharding(); - final List failingPackages = []; - await for (Directory plugin in getPlugins()) { - for (Directory example in getExamplesForPlugin(plugin)) { - final String packageName = - p.relative(example.path, from: packagesDir.path); - - if (argResults[kLinux]) { - print('\nBUILDING Linux for $packageName'); - if (isLinuxPlugin(plugin, fileSystem)) { - int buildExitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kLinux, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (buildExitCode != 0) { - failingPackages.add('$packageName (linux)'); - } - } else { - print('Linux is not supported by this plugin'); - } - } - - if (argResults[kMacos]) { - print('\nBUILDING macOS for $packageName'); - if (isMacOsPlugin(plugin, fileSystem)) { - // TODO(https://github.com/flutter/flutter/issues/46236): - // Builing macos without running flutter pub get first results - // in an error. - int exitCode = await processRunner.runAndStream( - flutterCommand, ['pub', 'get'], - workingDir: example); - if (exitCode != 0) { - failingPackages.add('$packageName (macos)'); - } else { - exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kMacos, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (exitCode != 0) { - failingPackages.add('$packageName (macos)'); - } - } - } else { - print('macOS is not supported by this plugin'); - } - } - - if (argResults[kWindows]) { - print('\nBUILDING Windows for $packageName'); - if (isWindowsPlugin(plugin, fileSystem)) { - int buildExitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kWindows, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (buildExitCode != 0) { - failingPackages.add('$packageName (windows)'); - } - } else { - print('Windows is not supported by this plugin'); - } - } - - if (argResults[kIpa]) { - print('\nBUILDING IPA for $packageName'); - if (isIosPlugin(plugin, fileSystem)) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - 'ios', - '--no-codesign', - '--verbose', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example, - exitOnError: true); - if (exitCode != 0) { - failingPackages.add('$packageName (ipa)'); - } - } else { - print('iOS is not supported by this plugin'); - } - } - print('end ios build'); - - if (argResults[kApk]) { - print('\nBUILDING APK for $packageName'); - if (isAndroidPlugin(plugin, fileSystem)) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - 'apk', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (exitCode != 0) { - failingPackages.add('$packageName (apk)'); - } - } else { - print('Android is not supported by this plugin'); - } - } - } - } - print('\n\n'); - - if (failingPackages.isNotEmpty) { - print('The following build are failing (see above for details):'); - for (String package in failingPackages) { - print(' * $package'); - } - throw ToolExit(1); - } - - print('All builds successful!'); - } -} diff --git a/packages/fluttter_plugin_tools/lib/src/common.dart b/packages/fluttter_plugin_tools/lib/src/common.dart deleted file mode 100644 index 78b91ee8a75b..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/common.dart +++ /dev/null @@ -1,466 +0,0 @@ -// 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 'dart:io' as io; -import 'dart:math'; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:yaml/yaml.dart'; - -typedef void Print(Object object); - -/// Key for windows platform. -const String kWindows = 'windows'; - -/// Key for macos platform. -const String kMacos = 'macos'; - -/// Key for linux platform. -const String kLinux = 'linux'; - -/// Key for IPA (iOS) platform. -const String kIos = 'ios'; - -/// Key for APK (Android) platform. -const String kAndroid = 'android'; - -/// Key for Web platform. -const String kWeb = 'web'; - -/// Key for IPA. -const String kIpa = 'ipa'; - -/// Key for APK. -const String kApk = 'apk'; - -/// Key for enable experiment. -const String kEnableExperiment = 'enable-experiment'; - -/// Returns whether the given directory contains a Flutter package. -bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) { - if (entity == null || entity is! Directory) { - return false; - } - - try { - final File pubspecFile = - fileSystem.file(p.join(entity.path, 'pubspec.yaml')); - final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); - final YamlMap dependencies = pubspecYaml['dependencies']; - if (dependencies == null) { - return false; - } - return dependencies.containsKey('flutter'); - } on FileSystemException { - return false; - } on YamlException { - return false; - } -} - -/// Returns whether the given directory contains a Flutter [platform] plugin. -/// -/// It checks this by looking for the following pattern in the pubspec: -/// -/// flutter: -/// plugin: -/// platforms: -/// [platform]: -bool pluginSupportsPlatform( - String platform, FileSystemEntity entity, FileSystem fileSystem) { - assert(platform == kIos || - platform == kAndroid || - platform == kWeb || - platform == kMacos || - platform == kWindows || - platform == kLinux); - if (entity == null || entity is! Directory) { - return false; - } - - try { - final File pubspecFile = - fileSystem.file(p.join(entity.path, 'pubspec.yaml')); - final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); - final YamlMap flutterSection = pubspecYaml['flutter']; - if (flutterSection == null) { - return false; - } - final YamlMap pluginSection = flutterSection['plugin']; - if (pluginSection == null) { - return false; - } - final YamlMap platforms = pluginSection['platforms']; - if (platforms == null) { - // Legacy plugin specs are assumed to support iOS and Android. - if (!pluginSection.containsKey('platforms')) { - return platform == kIos || platform == kAndroid; - } - return false; - } - return platforms.containsKey(platform); - } on FileSystemException { - return false; - } on YamlException { - return false; - } -} - -/// Returns whether the given directory contains a Flutter Android plugin. -bool isAndroidPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kAndroid, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter iOS plugin. -bool isIosPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kIos, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter web plugin. -bool isWebPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kWeb, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter Windows plugin. -bool isWindowsPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kWindows, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter macOS plugin. -bool isMacOsPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kMacos, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter linux plugin. -bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kLinux, entity, fileSystem); -} - -/// Error thrown when a command needs to exit with a non-zero exit code. -class ToolExit extends Error { - ToolExit(this.exitCode); - - final int exitCode; -} - -abstract class PluginCommand extends Command { - PluginCommand( - this.packagesDir, - this.fileSystem, { - this.processRunner = const ProcessRunner(), - }) { - argParser.addMultiOption( - _pluginsArg, - splitCommas: true, - help: - 'Specifies which plugins the command should run on (before sharding).', - valueHelp: 'plugin1,plugin2,...', - ); - argParser.addOption( - _shardIndexArg, - help: 'Specifies the zero-based index of the shard to ' - 'which the command applies.', - valueHelp: 'i', - defaultsTo: '0', - ); - argParser.addOption( - _shardCountArg, - help: 'Specifies the number of shards into which plugins are divided.', - valueHelp: 'n', - defaultsTo: '1', - ); - argParser.addMultiOption( - _excludeArg, - abbr: 'e', - help: 'Exclude packages from this command.', - defaultsTo: [], - ); - } - - static const String _pluginsArg = 'plugins'; - static const String _shardIndexArg = 'shardIndex'; - static const String _shardCountArg = 'shardCount'; - static const String _excludeArg = 'exclude'; - - /// The directory containing the plugin packages. - final Directory packagesDir; - - /// The file system. - /// - /// This can be overridden for testing. - final FileSystem fileSystem; - - /// The process runner. - /// - /// This can be overridden for testing. - final ProcessRunner processRunner; - - int _shardIndex; - int _shardCount; - - int get shardIndex { - if (_shardIndex == null) { - checkSharding(); - } - return _shardIndex; - } - - int get shardCount { - if (_shardCount == null) { - checkSharding(); - } - return _shardCount; - } - - void checkSharding() { - final int shardIndex = int.tryParse(argResults[_shardIndexArg]); - final int shardCount = int.tryParse(argResults[_shardCountArg]); - if (shardIndex == null) { - usageException('$_shardIndexArg must be an integer'); - } - if (shardCount == null) { - usageException('$_shardCountArg must be an integer'); - } - if (shardCount < 1) { - usageException('$_shardCountArg must be positive'); - } - if (shardIndex < 0 || shardCount <= shardIndex) { - usageException( - '$_shardIndexArg must be in the half-open range [0..$shardCount['); - } - _shardIndex = shardIndex; - _shardCount = shardCount; - } - - /// Returns the root Dart package folders of the plugins involved in this - /// command execution. - Stream getPlugins() async* { - // To avoid assuming consistency of `Directory.list` across command - // invocations, we collect and sort the plugin folders before sharding. - // This is considered an implementation detail which is why the API still - // uses streams. - final List allPlugins = await _getAllPlugins().toList(); - allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path)); - // Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2. - // Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3. - // Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0. - final int shardSize = allPlugins.length ~/ shardCount + - (allPlugins.length % shardCount == 0 ? 0 : 1); - final int start = min(shardIndex * shardSize, allPlugins.length); - final int end = min(start + shardSize, allPlugins.length); - - for (Directory plugin in allPlugins.sublist(start, end)) { - yield plugin; - } - } - - /// Returns the root Dart package folders of the plugins involved in this - /// command execution, assuming there is only one shard. - /// - /// Plugin packages can exist in one of two places relative to the packages - /// directory. - /// - /// 1. As a Dart package in a directory which is a direct child of the - /// packages directory. This is a plugin where all of the implementations - /// exist in a single Dart package. - /// 2. Several plugin packages may live in a directory which is a direct - /// child of the packages directory. This directory groups several Dart - /// packages which implement a single plugin. This directory contains a - /// "client library" package, which declares the API for the plugin, as - /// well as one or more platform-specific implementations. - Stream _getAllPlugins() async* { - final Set plugins = Set.from(argResults[_pluginsArg]); - final Set excludedPlugins = - Set.from(argResults[_excludeArg]); - - await for (FileSystemEntity entity - in packagesDir.list(followLinks: false)) { - // A top-level Dart package is a plugin package. - if (_isDartPackage(entity)) { - if (!excludedPlugins.contains(entity.basename) && - (plugins.isEmpty || plugins.contains(p.basename(entity.path)))) { - yield entity; - } - } else if (entity is Directory) { - // Look for Dart packages under this top-level directory. - await for (FileSystemEntity subdir in entity.list(followLinks: false)) { - if (_isDartPackage(subdir)) { - // If --plugin=my_plugin is passed, then match all federated - // plugins under 'my_plugin'. Also match if the exact plugin is - // passed. - final String relativePath = - p.relative(subdir.path, from: packagesDir.path); - final String basenamePath = p.basename(entity.path); - if (!excludedPlugins.contains(basenamePath) && - !excludedPlugins.contains(relativePath) && - (plugins.isEmpty || - plugins.contains(relativePath) || - plugins.contains(basenamePath))) { - yield subdir; - } - } - } - } - } - } - - /// Returns the example Dart package folders of the plugins involved in this - /// command execution. - Stream getExamples() => - getPlugins().expand(getExamplesForPlugin); - - /// Returns all Dart package folders (typically, plugin + example) of the - /// plugins involved in this command execution. - Stream getPackages() async* { - await for (Directory plugin in getPlugins()) { - yield plugin; - yield* plugin - .list(recursive: true, followLinks: false) - .where(_isDartPackage) - .cast(); - } - } - - /// Returns the files contained, recursively, within the plugins - /// involved in this command execution. - Stream getFiles() { - return getPlugins().asyncExpand((Directory folder) => folder - .list(recursive: true, followLinks: false) - .where((FileSystemEntity entity) => entity is File) - .cast()); - } - - /// Returns whether the specified entity is a directory containing a - /// `pubspec.yaml` file. - bool _isDartPackage(FileSystemEntity entity) { - return entity is Directory && - fileSystem.file(p.join(entity.path, 'pubspec.yaml')).existsSync(); - } - - /// Returns the example Dart packages contained in the specified plugin, or - /// an empty List, if the plugin has no examples. - Iterable getExamplesForPlugin(Directory plugin) { - final Directory exampleFolder = - fileSystem.directory(p.join(plugin.path, 'example')); - if (!exampleFolder.existsSync()) { - return []; - } - if (isFlutterPackage(exampleFolder, fileSystem)) { - return [exampleFolder]; - } - // Only look at the subdirectories of the example directory if the example - // directory itself is not a Dart package, and only look one level below the - // example directory for other dart packages. - return exampleFolder - .listSync() - .where( - (FileSystemEntity entity) => isFlutterPackage(entity, fileSystem)) - .cast(); - } -} - -/// A class used to run processes. -/// -/// We use this instead of directly running the process so it can be overridden -/// in tests. -class ProcessRunner { - const ProcessRunner(); - - /// Run the [executable] with [args] and stream output to stderr and stdout. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// If [exitOnError] is set to `true`, then this will throw an error if - /// the [executable] terminates with a non-zero exit code. - /// - /// Returns the exit code of the [executable]. - Future runAndStream( - String executable, - List args, { - Directory workingDir, - bool exitOnError = false, - }) async { - print( - 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); - final io.Process process = await io.Process.start(executable, args, - workingDirectory: workingDir?.path); - await io.stdout.addStream(process.stdout); - await io.stderr.addStream(process.stderr); - if (exitOnError && await process.exitCode != 0) { - final String error = - _getErrorString(executable, args, workingDir: workingDir); - print('$error See above for details.'); - throw ToolExit(await process.exitCode); - } - return process.exitCode; - } - - /// Run the [executable] with [args]. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// If [exitOnError] is set to `true`, then this will throw an error if - /// the [executable] terminates with a non-zero exit code. - /// - /// Returns the [io.ProcessResult] of the [executable]. - Future run(String executable, List args, - {Directory workingDir, - bool exitOnError = false, - stdoutEncoding = io.systemEncoding, - stderrEncoding = io.systemEncoding}) async { - return io.Process.run(executable, args, - workingDirectory: workingDir?.path, - stdoutEncoding: stdoutEncoding, - stderrEncoding: stderrEncoding); - } - - /// Starts the [executable] with [args]. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// Returns the started [io.Process]. - Future start(String executable, List args, - {Directory workingDirectory}) async { - final io.Process process = await io.Process.start(executable, args, - workingDirectory: workingDirectory?.path); - return process; - } - - /// Run the [executable] with [args], throwing an error on non-zero exit code. - /// - /// Unlike [runAndStream], this does not stream the process output to stdout. - /// It also unconditionally throws an error on a non-zero exit code. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// Returns the [io.ProcessResult] of running the [executable]. - Future runAndExitOnError( - String executable, - List args, { - Directory workingDir, - }) async { - final io.ProcessResult result = await io.Process.run(executable, args, - workingDirectory: workingDir?.path); - if (result.exitCode != 0) { - final String error = - _getErrorString(executable, args, workingDir: workingDir); - print('$error Stderr:\n${result.stdout}'); - throw ToolExit(result.exitCode); - } - return result; - } - - String _getErrorString(String executable, List args, - {Directory workingDir}) { - final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; - return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; - } -} diff --git a/packages/fluttter_plugin_tools/lib/src/create_all_plugins_app_command.dart b/packages/fluttter_plugin_tools/lib/src/create_all_plugins_app_command.dart deleted file mode 100644 index 0f1431c5aee0..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/create_all_plugins_app_command.dart +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; - -import 'common.dart'; - -// TODO(cyanglaz): Add tests for this command. -// https://github.com/flutter/flutter/issues/61049 -class CreateAllPluginsAppCommand extends PluginCommand { - CreateAllPluginsAppCommand(Directory packagesDir, FileSystem fileSystem) - : super(packagesDir, fileSystem); - - @override - String get description => - 'Generate Flutter app that includes all plugins in packages.'; - - @override - String get name => 'all-plugins-app'; - - @override - Future run() async { - final int exitCode = await _createPlugin(); - if (exitCode != 0) { - throw ToolExit(exitCode); - } - - await Future.wait(>[ - _genPubspecWithAllPlugins(), - _updateAppGradle(), - _updateManifest(), - ]); - } - - Future _createPlugin() async { - final io.ProcessResult result = io.Process.runSync( - 'flutter', - [ - 'create', - '--template=app', - '--project-name=all_plugins', - '--android-language=java', - './all_plugins', - ], - ); - - print(result.stdout); - print(result.stderr); - return result.exitCode; - } - - Future _updateAppGradle() async { - final File gradleFile = fileSystem.file(p.join( - 'all_plugins', - 'android', - 'app', - 'build.gradle', - )); - if (!gradleFile.existsSync()) { - throw ToolExit(64); - } - - final StringBuffer newGradle = StringBuffer(); - for (String line in gradleFile.readAsLinesSync()) { - newGradle.writeln(line); - if (line.contains('defaultConfig {')) { - newGradle.writeln(' multiDexEnabled true'); - } else if (line.contains('dependencies {')) { - newGradle.writeln( - ' implementation \'com.google.guava:guava:27.0.1-android\'\n', - ); - // Tests for https://github.com/flutter/flutter/issues/43383 - newGradle.writeln( - " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", - ); - } - } - gradleFile.writeAsStringSync(newGradle.toString()); - } - - Future _updateManifest() async { - final File manifestFile = fileSystem.file(p.join( - 'all_plugins', - 'android', - 'app', - 'src', - 'main', - 'AndroidManifest.xml', - )); - if (!manifestFile.existsSync()) { - throw ToolExit(64); - } - - final StringBuffer newManifest = StringBuffer(); - for (String line in manifestFile.readAsLinesSync()) { - if (line.contains('package="com.example.all_plugins"')) { - newManifest - ..writeln('package="com.example.all_plugins"') - ..writeln('xmlns:tools="http://schemas.android.com/tools">') - ..writeln() - ..writeln( - '', - ); - } else { - newManifest.writeln(line); - } - } - manifestFile.writeAsStringSync(newManifest.toString()); - } - - Future _genPubspecWithAllPlugins() async { - final Map pluginDeps = - await _getValidPathDependencies(); - final Pubspec pubspec = Pubspec( - 'all_plugins', - description: 'Flutter app containing all 1st party plugins.', - version: Version.parse('1.0.0+1'), - environment: { - 'sdk': VersionConstraint.compatibleWith( - Version.parse('2.0.0'), - ), - }, - dependencies: { - 'flutter': SdkDependency('flutter'), - }..addAll(pluginDeps), - devDependencies: { - 'flutter_test': SdkDependency('flutter'), - }, - dependencyOverrides: pluginDeps, - ); - final File pubspecFile = - fileSystem.file(p.join('all_plugins', 'pubspec.yaml')); - pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); - } - - Future> _getValidPathDependencies() async { - final Map pathDependencies = - {}; - - await for (Directory package in getPlugins()) { - final String pluginName = package.path.split('/').last; - final File pubspecFile = - fileSystem.file(p.join(package.path, 'pubspec.yaml')); - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); - - if (pubspec.publishTo != 'none') { - pathDependencies[pluginName] = PathDependency(package.path); - } - } - return pathDependencies; - } - - String _pubspecToString(Pubspec pubspec) { - return ''' -### Generated file. Do not edit. Run `pub global run flutter_plugin_tools gen-pubspec` to update. -name: ${pubspec.name} -description: ${pubspec.description} - -version: ${pubspec.version} - -environment:${_pubspecMapString(pubspec.environment)} - -dependencies:${_pubspecMapString(pubspec.dependencies)} - -dependency_overrides:${_pubspecMapString(pubspec.dependencyOverrides)} - -dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} -###'''; - } - - String _pubspecMapString(Map values) { - final StringBuffer buffer = StringBuffer(); - - for (MapEntry entry in values.entries) { - buffer.writeln(); - if (entry.value is VersionConstraint) { - buffer.write(' ${entry.key}: ${entry.value}'); - } else if (entry.value is SdkDependency) { - final SdkDependency dep = entry.value; - buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); - } else if (entry.value is PathDependency) { - final PathDependency dep = entry.value; - buffer.write(' ${entry.key}: \n path: ${dep.path}'); - } else { - throw UnimplementedError( - 'Not available for type: ${entry.value.runtimeType}', - ); - } - } - - return buffer.toString(); - } -} diff --git a/packages/fluttter_plugin_tools/lib/src/drive_examples_command.dart b/packages/fluttter_plugin_tools/lib/src/drive_examples_command.dart deleted file mode 100644 index 8b1fa3624fbf..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/drive_examples_command.dart +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'common.dart'; - -class DriveExamplesCommand extends PluginCommand { - DriveExamplesCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addFlag(kLinux, - help: 'Runs the Linux implementation of the examples'); - argParser.addFlag(kMacos, - help: 'Runs the macOS implementation of the examples'); - argParser.addFlag(kWindows, - help: 'Runs the Windows implementation of the examples'); - argParser.addFlag(kIos, - help: 'Runs the iOS implementation of the examples'); - argParser.addFlag(kAndroid, - help: 'Runs the Android implementation of the examples'); - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: - 'Runs the driver tests in Dart VM with the given experiments enabled.', - ); - } - - @override - final String name = 'drive-examples'; - - @override - final String description = 'Runs driver tests for plugin example apps.\n\n' - 'For each *_test.dart in test_driver/ it drives an application with a ' - 'corresponding name in the test/ or test_driver/ directories.\n\n' - 'For example, test_driver/app_test.dart would match test/app.dart.\n\n' - 'This command requires "flutter" to be in your path.\n\n' - 'If a file with a corresponding name cannot be found, this driver file' - 'will be used to drive the tests that match ' - 'integration_test/*_test.dart.'; - - @override - Future run() async { - checkSharding(); - final List failingTests = []; - final bool isLinux = argResults[kLinux]; - final bool isMacos = argResults[kMacos]; - final bool isWindows = argResults[kWindows]; - await for (Directory plugin in getPlugins()) { - final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - for (Directory example in getExamplesForPlugin(plugin)) { - final String packageName = - p.relative(example.path, from: packagesDir.path); - if (!(await pluginSupportedOnCurrentPlatform(plugin, fileSystem))) { - continue; - } - final Directory driverTests = - fileSystem.directory(p.join(example.path, 'test_driver')); - if (!driverTests.existsSync()) { - // No driver tests available for this example - continue; - } - // Look for driver tests ending in _test.dart in test_driver/ - await for (FileSystemEntity test in driverTests.list()) { - final String driverTestName = - p.relative(test.path, from: driverTests.path); - if (!driverTestName.endsWith('_test.dart')) { - continue; - } - // Try to find a matching app to drive without the _test.dart - final String deviceTestName = driverTestName.replaceAll( - RegExp(r'_test.dart$'), - '.dart', - ); - String deviceTestPath = p.join('test', deviceTestName); - if (!fileSystem - .file(p.join(example.path, deviceTestPath)) - .existsSync()) { - // If the app isn't in test/ folder, look in test_driver/ instead. - deviceTestPath = p.join('test_driver', deviceTestName); - } - - final List targetPaths = []; - if (fileSystem - .file(p.join(example.path, deviceTestPath)) - .existsSync()) { - targetPaths.add(deviceTestPath); - } else { - final Directory integrationTests = - fileSystem.directory(p.join(example.path, 'integration_test')); - - if (await integrationTests.exists()) { - await for (FileSystemEntity integration_test - in integrationTests.list()) { - if (!integration_test.basename.endsWith('_test.dart')) { - continue; - } - targetPaths - .add(p.relative(integration_test.path, from: example.path)); - } - } - - if (targetPaths.isEmpty) { - print(''' -Unable to infer a target application for $driverTestName to drive. -Tried searching for the following: -1. test/$deviceTestName -2. test_driver/$deviceTestName -3. test_driver/*_test.dart -'''); - failingTests.add(p.relative(test.path, from: example.path)); - continue; - } - } - - final List driveArgs = ['drive', '-v']; - - final String enableExperiment = argResults[kEnableExperiment]; - if (enableExperiment.isNotEmpty) { - driveArgs.add('--enable-experiment=$enableExperiment'); - } - - if (isLinux && isLinuxPlugin(plugin, fileSystem)) { - driveArgs.addAll([ - '-d', - 'linux', - ]); - } - if (isMacos && isMacOsPlugin(plugin, fileSystem)) { - driveArgs.addAll([ - '-d', - 'macos', - ]); - } - if (isWindows && isWindowsPlugin(plugin, fileSystem)) { - driveArgs.addAll([ - '-d', - 'windows', - ]); - } - - for (final targetPath in targetPaths) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - ...driveArgs, - '--driver', - p.join('test_driver', driverTestName), - '--target', - targetPath, - ], - workingDir: example, - exitOnError: true); - if (exitCode != 0) { - failingTests.add(p.join(packageName, deviceTestPath)); - } - } - } - } - } - print('\n\n'); - - if (failingTests.isNotEmpty) { - print('The following driver tests are failing (see above for details):'); - for (String test in failingTests) { - print(' * $test'); - } - throw ToolExit(1); - } - - print('All driver tests successful!'); - } - - Future pluginSupportedOnCurrentPlatform( - FileSystemEntity plugin, FileSystem fileSystem) async { - final bool isLinux = argResults[kLinux]; - final bool isMacos = argResults[kMacos]; - final bool isWindows = argResults[kWindows]; - final bool isIOS = argResults[kIos]; - final bool isAndroid = argResults[kAndroid]; - if (isLinux) { - return isLinuxPlugin(plugin, fileSystem); - } - if (isMacos) { - return isMacOsPlugin(plugin, fileSystem); - } - if (isWindows) { - return isWindowsPlugin(plugin, fileSystem); - } - if (isIOS) { - return isIosPlugin(plugin, fileSystem); - } - if (isAndroid) { - return (isAndroidPlugin(plugin, fileSystem)); - } - // When we are here, no flags are specified. Only return true if the plugin supports mobile for legacy command support. - // TODO(cyanglaz): Make mobile platforms flags also required like other platforms (breaking change). - // https://github.com/flutter/flutter/issues/58285 - final bool isMobilePlugin = - isIosPlugin(plugin, fileSystem) || isAndroidPlugin(plugin, fileSystem); - return isMobilePlugin; - } -} diff --git a/packages/fluttter_plugin_tools/lib/src/firebase_test_lab_command.dart b/packages/fluttter_plugin_tools/lib/src/firebase_test_lab_command.dart deleted file mode 100644 index 0b4b2a471dbc..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/firebase_test_lab_command.dart +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:uuid/uuid.dart'; - -import 'common.dart'; - -class FirebaseTestLabCommand extends PluginCommand { - FirebaseTestLabCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - Print print = print, - }) : _print = print, - super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption( - 'project', - defaultsTo: 'flutter-infra', - help: 'The Firebase project name.', - ); - argParser.addOption('service-key', - defaultsTo: - p.join(io.Platform.environment['HOME'], 'gcloud-service-key.json')); - argParser.addOption('test-run-id', - defaultsTo: Uuid().v4(), - help: - 'Optional string to append to the results path, to avoid conflicts. ' - 'Randomly chosen on each invocation if none is provided. ' - 'The default shown here is just an example.'); - argParser.addMultiOption('device', - splitCommas: false, - defaultsTo: [ - 'model=walleye,version=26', - 'model=flame,version=29' - ], - help: - 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); - argParser.addOption('results-bucket', - defaultsTo: 'gs://flutter_firebase_testlab'); - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: 'Enables the given Dart SDK experiments.', - ); - } - - @override - final String name = 'firebase-test-lab'; - - @override - final String description = 'Runs the instrumentation tests of the example ' - 'apps on Firebase Test Lab.\n\n' - 'Runs tests in test_instrumentation folder using the ' - 'instrumentation_test package.'; - - static const String _gradleWrapper = 'gradlew'; - - final Print _print; - - Completer _firebaseProjectConfigured; - - Future _configureFirebaseProject() async { - if (_firebaseProjectConfigured != null) { - return _firebaseProjectConfigured.future; - } else { - _firebaseProjectConfigured = Completer(); - } - await processRunner.runAndExitOnError('gcloud', [ - 'auth', - 'activate-service-account', - '--key-file=${argResults['service-key']}', - ]); - int exitCode = await processRunner.runAndStream('gcloud', [ - 'config', - 'set', - 'project', - argResults['project'], - ]); - if (exitCode == 0) { - _print('\nFirebase project configured.'); - return; - } else { - _print( - '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.'); - } - _firebaseProjectConfigured.complete(null); - } - - @override - Future run() async { - checkSharding(); - final Stream packagesWithTests = getPackages().where( - (Directory d) => - isFlutterPackage(d, fileSystem) && - fileSystem - .directory(p.join( - d.path, 'example', 'android', 'app', 'src', 'androidTest')) - .existsSync()); - - final List failingPackages = []; - final List missingFlutterBuild = []; - int resultsCounter = - 0; // We use a unique GCS bucket for each Firebase Test Lab run - await for (Directory package in packagesWithTests) { - // See https://github.com/flutter/flutter/issues/38983 - - final Directory exampleDirectory = - fileSystem.directory(p.join(package.path, 'example')); - final String packageName = - p.relative(package.path, from: packagesDir.path); - _print('\nRUNNING FIREBASE TEST LAB TESTS for $packageName'); - - final Directory androidDirectory = - fileSystem.directory(p.join(exampleDirectory.path, 'android')); - - final String enableExperiment = argResults[kEnableExperiment]; - final String encodedEnableExperiment = - Uri.encodeComponent('--enable-experiment=$enableExperiment'); - - // Ensures that gradle wrapper exists - if (!fileSystem - .file(p.join(androidDirectory.path, _gradleWrapper)) - .existsSync()) { - final int exitCode = await processRunner.runAndStream( - 'flutter', - [ - 'build', - 'apk', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: androidDirectory); - - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - continue; - } - - await _configureFirebaseProject(); - - int exitCode = await processRunner.runAndStream( - p.join(androidDirectory.path, _gradleWrapper), - [ - 'app:assembleAndroidTest', - '-Pverbose=true', - if (enableExperiment.isNotEmpty) - '-Pextra-front-end-options=$encodedEnableExperiment', - if (enableExperiment.isNotEmpty) - '-Pextra-gen-snapshot-options=$encodedEnableExperiment', - ], - workingDir: androidDirectory); - - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - - // Look for tests recursively in folders that start with 'test' and that - // live in the root or example folders. - bool isTestDir(FileSystemEntity dir) { - return p.basename(dir.path).startsWith('test') || - p.basename(dir.path) == 'integration_test'; - } - - final List testDirs = - package.listSync().where(isTestDir).toList(); - final Directory example = - fileSystem.directory(p.join(package.path, 'example')); - testDirs.addAll(example.listSync().where(isTestDir).toList()); - for (Directory testDir in testDirs) { - bool isE2ETest(FileSystemEntity file) { - return file.path.endsWith('_e2e.dart') || - (file.parent.basename == 'integration_test' && - file.path.endsWith('_test.dart')); - } - - final List testFiles = testDir - .listSync(recursive: true, followLinks: true) - .where(isE2ETest) - .toList(); - for (FileSystemEntity test in testFiles) { - exitCode = await processRunner.runAndStream( - p.join(androidDirectory.path, _gradleWrapper), - [ - 'app:assembleDebug', - '-Pverbose=true', - '-Ptarget=${test.path}', - if (enableExperiment.isNotEmpty) - '-Pextra-front-end-options=$encodedEnableExperiment', - if (enableExperiment.isNotEmpty) - '-Pextra-gen-snapshot-options=$encodedEnableExperiment', - ], - workingDir: androidDirectory); - - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - final String buildId = io.Platform.environment['CIRRUS_BUILD_ID']; - final String testRunId = argResults['test-run-id']; - final String resultsDir = - 'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/'; - final List args = [ - 'firebase', - 'test', - 'android', - 'run', - '--type', - 'instrumentation', - '--app', - 'build/app/outputs/apk/debug/app-debug.apk', - '--test', - 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', - '--timeout', - '5m', - '--results-bucket=${argResults['results-bucket']}', - '--results-dir=${resultsDir}', - ]; - for (String device in argResults['device']) { - args.addAll(['--device', device]); - } - exitCode = await processRunner.runAndStream('gcloud', args, - workingDir: exampleDirectory); - - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - } - } - } - - _print('\n\n'); - if (failingPackages.isNotEmpty) { - _print( - 'The instrumentation tests for the following packages are failing (see above for' - 'details):'); - for (String package in failingPackages) { - _print(' * $package'); - } - } - if (missingFlutterBuild.isNotEmpty) { - _print('Run "pub global run flutter_plugin_tools build-examples --apk" on' - 'the following packages before executing tests again:'); - for (String package in missingFlutterBuild) { - _print(' * $package'); - } - } - - if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { - throw ToolExit(1); - } - - _print('All Firebase Test Lab tests successful!'); - } -} diff --git a/packages/fluttter_plugin_tools/lib/src/format_command.dart b/packages/fluttter_plugin_tools/lib/src/format_command.dart deleted file mode 100644 index ec326b96c1f9..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/format_command.dart +++ /dev/null @@ -1,147 +0,0 @@ -// 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 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as p; -import 'package:quiver/iterables.dart'; - -import 'common.dart'; - -const String _googleFormatterUrl = - 'https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'; - -class FormatCommand extends PluginCommand { - FormatCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addFlag('travis', hide: true); - argParser.addOption('clang-format', - defaultsTo: 'clang-format', - help: 'Path to executable of clang-format v5.'); - } - - @override - final String name = 'format'; - - @override - final String description = - 'Formats the code of all packages (Java, Objective-C, C++, and Dart).\n\n' - 'This command requires "git", "flutter" and "clang-format" v5 to be in ' - 'your path.'; - - @override - Future run() async { - checkSharding(); - final String googleFormatterPath = await _getGoogleFormatterPath(); - - await _formatDart(); - await _formatJava(googleFormatterPath); - await _formatCppAndObjectiveC(); - - if (argResults['travis']) { - final bool modified = await _didModifyAnything(); - if (modified) { - throw ToolExit(1); - } - } - } - - Future _didModifyAnything() async { - final io.ProcessResult modifiedFiles = await processRunner - .runAndExitOnError('git', ['ls-files', '--modified'], - workingDir: packagesDir); - - print('\n\n'); - - if (modifiedFiles.stdout.isEmpty) { - print('All files formatted correctly.'); - return false; - } - - print('These files are not formatted correctly (see diff below):'); - LineSplitter.split(modifiedFiles.stdout) - .map((String line) => ' $line') - .forEach(print); - - print('\nTo fix run "pub global activate flutter_plugin_tools && ' - 'pub global run flutter_plugin_tools format" or copy-paste ' - 'this command into your terminal:'); - - print('patch -p1 <['diff'], workingDir: packagesDir); - print(diff.stdout); - print('DONE'); - return true; - } - - Future _formatCppAndObjectiveC() async { - print('Formatting all .cc, .cpp, .mm, .m, and .h files...'); - final Iterable allFiles = [] - ..addAll(await _getFilesWithExtension('.h')) - ..addAll(await _getFilesWithExtension('.m')) - ..addAll(await _getFilesWithExtension('.mm')) - ..addAll(await _getFilesWithExtension('.cc')) - ..addAll(await _getFilesWithExtension('.cpp')); - // Split this into multiple invocations to avoid a - // 'ProcessException: Argument list too long'. - final Iterable> batches = partition(allFiles, 100); - for (List batch in batches) { - await processRunner.runAndStream(argResults['clang-format'], - ['-i', '--style=Google']..addAll(batch), - workingDir: packagesDir, exitOnError: true); - } - } - - Future _formatJava(String googleFormatterPath) async { - print('Formatting all .java files...'); - final Iterable javaFiles = await _getFilesWithExtension('.java'); - await processRunner.runAndStream('java', - ['-jar', googleFormatterPath, '--replace']..addAll(javaFiles), - workingDir: packagesDir, exitOnError: true); - } - - Future _formatDart() async { - // This actually should be fine for non-Flutter Dart projects, no need to - // specifically shell out to dartfmt -w in that case. - print('Formatting all .dart files...'); - final Iterable dartFiles = await _getFilesWithExtension('.dart'); - if (dartFiles.isEmpty) { - print( - 'No .dart files to format. If you set the `--exclude` flag, most likey they were skipped'); - } else { - await processRunner.runAndStream( - 'flutter', ['format']..addAll(dartFiles), - workingDir: packagesDir, exitOnError: true); - } - } - - Future> _getFilesWithExtension(String extension) async => - getFiles() - .where((File file) => p.extension(file.path) == extension) - .map((File file) => file.path) - .toList(); - - Future _getGoogleFormatterPath() async { - final String javaFormatterPath = p.join( - p.dirname(p.fromUri(io.Platform.script)), - 'google-java-format-1.3-all-deps.jar'); - final File javaFormatterFile = fileSystem.file(javaFormatterPath); - - if (!javaFormatterFile.existsSync()) { - print('Downloading Google Java Format...'); - final http.Response response = await http.get(_googleFormatterUrl); - javaFormatterFile.writeAsBytesSync(response.bodyBytes); - } - - return javaFormatterPath; - } -} diff --git a/packages/fluttter_plugin_tools/lib/src/java_test_command.dart b/packages/fluttter_plugin_tools/lib/src/java_test_command.dart deleted file mode 100644 index cf605bfc5ce2..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/java_test_command.dart +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; - -import 'common.dart'; - -class JavaTestCommand extends PluginCommand { - JavaTestCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner); - - @override - final String name = 'java-test'; - - @override - final String description = 'Runs the Java tests of the example apps.\n\n' - 'Building the apks of the example apps is required before executing this' - 'command.'; - - static const String _gradleWrapper = 'gradlew'; - - @override - Future run() async { - checkSharding(); - final Stream examplesWithTests = getExamples().where( - (Directory d) => - isFlutterPackage(d, fileSystem) && - fileSystem - .directory(p.join(d.path, 'android', 'app', 'src', 'test')) - .existsSync()); - - final List failingPackages = []; - final List missingFlutterBuild = []; - await for (Directory example in examplesWithTests) { - final String packageName = - p.relative(example.path, from: packagesDir.path); - print('\nRUNNING JAVA TESTS for $packageName'); - - final Directory androidDirectory = - fileSystem.directory(p.join(example.path, 'android')); - if (!fileSystem - .file(p.join(androidDirectory.path, _gradleWrapper)) - .existsSync()) { - print('ERROR: Run "flutter build apk" on example app of $packageName' - 'before executing tests.'); - missingFlutterBuild.add(packageName); - continue; - } - - final int exitCode = await processRunner.runAndStream( - p.join(androidDirectory.path, _gradleWrapper), - ['testDebugUnitTest', '--info'], - workingDir: androidDirectory); - if (exitCode != 0) { - failingPackages.add(packageName); - } - } - - print('\n\n'); - if (failingPackages.isNotEmpty) { - print( - 'The Java tests for the following packages are failing (see above for' - 'details):'); - for (String package in failingPackages) { - print(' * $package'); - } - } - if (missingFlutterBuild.isNotEmpty) { - print('Run "pub global run flutter_plugin_tools build-examples --apk" on' - 'the following packages before executing tests again:'); - for (String package in missingFlutterBuild) { - print(' * $package'); - } - } - - if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { - throw ToolExit(1); - } - - print('All Java tests successful!'); - } -} diff --git a/packages/fluttter_plugin_tools/lib/src/lint_podspecs_command.dart b/packages/fluttter_plugin_tools/lib/src/lint_podspecs_command.dart deleted file mode 100644 index 68fd4b61dd66..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/lint_podspecs_command.dart +++ /dev/null @@ -1,146 +0,0 @@ -// 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 'dart:convert'; -import 'dart:io'; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; - -import 'common.dart'; - -typedef void Print(Object object); - -/// Lint the CocoaPod podspecs, run the static analyzer on iOS/macOS plugin -/// platform code, and run unit tests. -/// -/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. -class LintPodspecsCommand extends PluginCommand { - LintPodspecsCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - this.platform = const LocalPlatform(), - Print print = print, - }) : _print = print, - super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addMultiOption('skip', - help: - 'Skip all linting for podspecs with this basename (example: federated plugins with placeholder podspecs)', - valueHelp: 'podspec_file_name'); - argParser.addMultiOption('ignore-warnings', - help: - 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs with this basename (example: plugins with known warnings)', - valueHelp: 'podspec_file_name'); - argParser.addMultiOption('no-analyze', - help: - 'Do not pass --analyze flag to "pod lib lint" for podspecs with this basename (example: plugins with known analyzer warnings)', - valueHelp: 'podspec_file_name'); - } - - @override - final String name = 'podspecs'; - - @override - List get aliases => ['podspec']; - - @override - final String description = - 'Runs "pod lib lint" on all iOS and macOS plugin podspecs.\n\n' - 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; - - final Platform platform; - - final Print _print; - - @override - Future run() async { - if (!platform.isMacOS) { - _print('Detected platform is not macOS, skipping podspec lint'); - return; - } - - checkSharding(); - - await processRunner.runAndExitOnError('which', ['pod'], - workingDir: packagesDir); - - _print('Starting podspec lint test'); - - final List failingPlugins = []; - for (File podspec in await _podspecsToLint()) { - if (!await _lintPodspec(podspec)) { - failingPlugins.add(p.basenameWithoutExtension(podspec.path)); - } - } - - _print('\n\n'); - if (failingPlugins.isNotEmpty) { - _print('The following plugins have podspec errors (see above):'); - failingPlugins.forEach((String plugin) { - _print(' * $plugin'); - }); - throw ToolExit(1); - } - } - - Future> _podspecsToLint() async { - final List podspecs = await getFiles().where((File entity) { - final String filePath = entity.path; - return p.extension(filePath) == '.podspec' && - !argResults['skip'].contains(p.basenameWithoutExtension(filePath)); - }).toList(); - - podspecs.sort( - (File a, File b) => p.basename(a.path).compareTo(p.basename(b.path))); - return podspecs; - } - - Future _lintPodspec(File podspec) async { - // Do not run the static analyzer on plugins with known analyzer issues. - final String podspecPath = podspec.path; - final bool runAnalyzer = !argResults['no-analyze'] - .contains(p.basenameWithoutExtension(podspecPath)); - - final String podspecBasename = p.basename(podspecPath); - if (runAnalyzer) { - _print('Linting and analyzing $podspecBasename'); - } else { - _print('Linting $podspecBasename'); - } - - // Lint plugin as framework (use_frameworks!). - final ProcessResult frameworkResult = await _runPodLint(podspecPath, - runAnalyzer: runAnalyzer, libraryLint: true); - _print(frameworkResult.stdout); - _print(frameworkResult.stderr); - - // Lint plugin as library. - final ProcessResult libraryResult = await _runPodLint(podspecPath, - runAnalyzer: runAnalyzer, libraryLint: false); - _print(libraryResult.stdout); - _print(libraryResult.stderr); - - return frameworkResult.exitCode == 0 && libraryResult.exitCode == 0; - } - - Future _runPodLint(String podspecPath, - {bool runAnalyzer, bool libraryLint}) async { - final bool allowWarnings = argResults['ignore-warnings'] - .contains(p.basenameWithoutExtension(podspecPath)); - final List arguments = [ - 'lib', - 'lint', - podspecPath, - if (allowWarnings) '--allow-warnings', - if (runAnalyzer) '--analyze', - if (libraryLint) '--use-libraries' - ]; - - return processRunner.run('pod', arguments, - workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); - } -} diff --git a/packages/fluttter_plugin_tools/lib/src/list_command.dart b/packages/fluttter_plugin_tools/lib/src/list_command.dart deleted file mode 100644 index 7f94daac7096..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/list_command.dart +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:file/file.dart'; - -import 'common.dart'; - -class ListCommand extends PluginCommand { - ListCommand(Directory packagesDir, FileSystem fileSystem) - : super(packagesDir, fileSystem) { - argParser.addOption( - _type, - defaultsTo: _plugin, - allowed: [_plugin, _example, _package, _file], - help: 'What type of file system content to list.', - ); - } - - static const String _type = 'type'; - static const String _plugin = 'plugin'; - static const String _example = 'example'; - static const String _package = 'package'; - static const String _file = 'file'; - - @override - final String name = 'list'; - - @override - final String description = 'Lists packages or files'; - - @override - Future run() async { - checkSharding(); - switch (argResults[_type]) { - case _plugin: - await for (Directory package in getPlugins()) { - print(package.path); - } - break; - case _example: - await for (Directory package in getExamples()) { - print(package.path); - } - break; - case _package: - await for (Directory package in getPackages()) { - print(package.path); - } - break; - case _file: - await for (File file in getFiles()) { - print(file.path); - } - break; - } - } -} diff --git a/packages/fluttter_plugin_tools/lib/src/main.dart b/packages/fluttter_plugin_tools/lib/src/main.dart deleted file mode 100644 index bb3f67c0a9e1..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/main.dart +++ /dev/null @@ -1,63 +0,0 @@ -// 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:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; -import 'package:path/path.dart' as p; - -import 'analyze_command.dart'; -import 'build_examples_command.dart'; -import 'common.dart'; -import 'create_all_plugins_app_command.dart'; -import 'drive_examples_command.dart'; -import 'firebase_test_lab_command.dart'; -import 'format_command.dart'; -import 'java_test_command.dart'; -import 'lint_podspecs_command.dart'; -import 'list_command.dart'; -import 'test_command.dart'; -import 'version_check_command.dart'; -import 'xctest_command.dart'; - -void main(List args) { - final FileSystem fileSystem = const LocalFileSystem(); - - Directory packagesDir = fileSystem - .directory(p.join(fileSystem.currentDirectory.path, 'packages')); - - if (!packagesDir.existsSync()) { - if (p.basename(fileSystem.currentDirectory.path) == 'packages') { - packagesDir = fileSystem.currentDirectory; - } else { - print('Error: Cannot find a "packages" sub-directory'); - io.exit(1); - } - } - - final CommandRunner commandRunner = CommandRunner( - 'pub global run flutter_plugin_tools', - 'Productivity utils for hosting multiple plugins within one repository.') - ..addCommand(AnalyzeCommand(packagesDir, fileSystem)) - ..addCommand(BuildExamplesCommand(packagesDir, fileSystem)) - ..addCommand(CreateAllPluginsAppCommand(packagesDir, fileSystem)) - ..addCommand(DriveExamplesCommand(packagesDir, fileSystem)) - ..addCommand(FirebaseTestLabCommand(packagesDir, fileSystem)) - ..addCommand(FormatCommand(packagesDir, fileSystem)) - ..addCommand(JavaTestCommand(packagesDir, fileSystem)) - ..addCommand(LintPodspecsCommand(packagesDir, fileSystem)) - ..addCommand(ListCommand(packagesDir, fileSystem)) - ..addCommand(PublishPluginCommand(packagesDir, fileSystem)) - ..addCommand(TestCommand(packagesDir, fileSystem)) - ..addCommand(VersionCheckCommand(packagesDir, fileSystem)) - ..addCommand(XCTestCommand(packagesDir, fileSystem)); - - commandRunner.run(args).catchError((Object e) { - final ToolExit toolExit = e; - io.exit(toolExit.exitCode); - }, test: (Object e) => e is ToolExit); -} diff --git a/packages/fluttter_plugin_tools/lib/src/publish_plugin_command.dart b/packages/fluttter_plugin_tools/lib/src/publish_plugin_command.dart deleted file mode 100644 index 55c48f5484e5..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/publish_plugin_command.dart +++ /dev/null @@ -1,227 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; -import 'package:yaml/yaml.dart'; - -import 'common.dart'; - -/// Wraps pub publish with a few niceties used by the flutter/plugin team. -/// -/// 1. Checks for any modified files in git and refuses to publish if there's an -/// issue. -/// 2. Tags the release with the format -v. -/// 3. Pushes the release to a remote. -/// -/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full -/// usage information. -/// -/// [processRunner], [print], and [stdin] can be overriden for easier testing. -class PublishPluginCommand extends PluginCommand { - PublishPluginCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - Print print = print, - Stdin stdinput, - }) : _print = print, - _stdin = stdinput ?? stdin, - super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption( - _packageOption, - help: 'The package to publish.' - 'If the package directory name is different than its pubspec.yaml name, then this should specify the directory.', - ); - argParser.addMultiOption(_pubFlagsOption, - help: - 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); - argParser.addFlag( - _tagReleaseOption, - help: 'Whether or not to tag the release.', - defaultsTo: true, - negatable: true, - ); - argParser.addFlag( - _pushTagsOption, - help: - 'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.', - defaultsTo: true, - negatable: true, - ); - argParser.addOption( - _remoteOption, - help: - 'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.', - // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. - defaultsTo: 'upstream', - ); - } - - static const String _packageOption = 'package'; - static const String _tagReleaseOption = 'tag-release'; - static const String _pushTagsOption = 'push-tags'; - static const String _pubFlagsOption = 'pub-publish-flags'; - static const String _remoteOption = 'remote'; - - // Version tags should follow -v. For example, - // `flutter_plugin_tools-v0.0.24`. - static const String _tagFormat = '%PACKAGE%-v%VERSION%'; - - @override - final String name = 'publish-plugin'; - - @override - final String description = - 'Attempts to publish the given plugin and tag its release on GitHub.'; - - final Print _print; - final Stdin _stdin; - // The directory of the actual package that we are publishing. - Directory _packageDir; - StreamSubscription _stdinSubscription; - - @override - Future run() async { - checkSharding(); - _print('Checking local repo...'); - _packageDir = _checkPackageDir(); - await _checkGitStatus(); - final bool shouldPushTag = argResults[_pushTagsOption]; - final String remote = argResults[_remoteOption]; - String remoteUrl; - if (shouldPushTag) { - remoteUrl = await _verifyRemote(remote); - } - _print('Local repo is ready!'); - - await _publish(); - _print('Package published!'); - if (!argResults[_tagReleaseOption]) { - return await _finishSuccesfully(); - } - - _print('Tagging release...'); - final String tag = _getTag(); - await processRunner.runAndExitOnError('git', ['tag', tag], - workingDir: _packageDir); - if (!shouldPushTag) { - return await _finishSuccesfully(); - } - - _print('Pushing tag to $remote...'); - await _pushTagToRemote(remote: remote, tag: tag, remoteUrl: remoteUrl); - await _finishSuccesfully(); - } - - Future _finishSuccesfully() async { - await _stdinSubscription.cancel(); - _print('Done!'); - } - - Directory _checkPackageDir() { - final String package = argResults[_packageOption]; - if (package == null) { - _print( - 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); - throw ToolExit(1); - } - final Directory _packageDir = packagesDir.childDirectory(package); - if (!_packageDir.existsSync()) { - _print('${_packageDir.absolute.path} does not exist.'); - throw ToolExit(1); - } - if (!isFlutterPackage(_packageDir, fileSystem)) { - _print('${_packageDir.absolute.path} is not a flutter package.'); - throw ToolExit(1); - } - return _packageDir; - } - - Future _checkGitStatus() async { - if (!await GitDir.isGitDir(packagesDir.path)) { - _print('$packagesDir is not a valid Git repository.'); - throw ToolExit(1); - } - - final ProcessResult statusResult = await processRunner.runAndExitOnError( - 'git', - [ - 'status', - '--porcelain', - '--ignored', - _packageDir.absolute.path - ], - workingDir: _packageDir); - final String statusOutput = statusResult.stdout; - if (statusOutput.isNotEmpty) { - _print( - "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" - '$statusOutput\n' - 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); - throw ToolExit(1); - } - } - - Future _verifyRemote(String remote) async { - final ProcessResult remoteInfo = await processRunner.runAndExitOnError( - 'git', ['remote', 'get-url', remote], - workingDir: _packageDir); - return remoteInfo.stdout; - } - - Future _publish() async { - final List publishFlags = argResults[_pubFlagsOption]; - _print( - 'Running `pub publish ${publishFlags.join(' ')}` in ${_packageDir.absolute.path}...\n'); - final Process publish = await processRunner.start( - 'flutter', ['pub', 'publish'] + publishFlags, - workingDirectory: _packageDir); - publish.stdout - .transform(utf8.decoder) - .listen((String data) => _print(data)); - publish.stderr - .transform(utf8.decoder) - .listen((String data) => _print(data)); - _stdinSubscription = _stdin - .transform(utf8.decoder) - .listen((String data) => publish.stdin.writeln(data)); - final int result = await publish.exitCode; - if (result != 0) { - _print('Publish failed. Exiting.'); - throw ToolExit(result); - } - } - - String _getTag() { - final File pubspecFile = - fileSystem.file(p.join(_packageDir.path, 'pubspec.yaml')); - final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); - final String name = pubspecYaml['name']; - final String version = pubspecYaml['version']; - // We should have failed to publish if these were unset. - assert(name.isNotEmpty && version.isNotEmpty); - return _tagFormat - .replaceAll('%PACKAGE%', name) - .replaceAll('%VERSION%', version); - } - - Future _pushTagToRemote( - {@required String remote, - @required String tag, - @required String remoteUrl}) async { - assert(remote != null && tag != null && remoteUrl != null); - _print('Ready to push $tag to $remoteUrl (y/n)?'); - final String input = _stdin.readLineSync(); - if (input.toLowerCase() != 'y') { - _print('Tag push canceled.'); - throw ToolExit(1); - } - - await processRunner.runAndExitOnError('git', ['push', remote, tag], - workingDir: packagesDir); - } -} diff --git a/packages/fluttter_plugin_tools/lib/src/test_command.dart b/packages/fluttter_plugin_tools/lib/src/test_command.dart deleted file mode 100644 index e938168cfa89..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/test_command.dart +++ /dev/null @@ -1,101 +0,0 @@ -// 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:file/file.dart'; -import 'package:path/path.dart' as p; - -import 'common.dart'; - -class TestCommand extends PluginCommand { - TestCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: 'Runs the tests in Dart VM with the given experiments enabled.', - ); - } - - @override - final String name = 'test'; - - @override - final String description = 'Runs the Dart tests for all packages.\n\n' - 'This command requires "flutter" to be in your path.'; - - @override - Future run() async { - checkSharding(); - final List failingPackages = []; - await for (Directory packageDir in getPackages()) { - final String packageName = - p.relative(packageDir.path, from: packagesDir.path); - if (!fileSystem.directory(p.join(packageDir.path, 'test')).existsSync()) { - print('SKIPPING $packageName - no test subdirectory'); - continue; - } - - print('RUNNING $packageName tests...'); - - final String enableExperiment = argResults[kEnableExperiment]; - - // `flutter test` automatically gets packages. `pub run test` does not. :( - int exitCode = 0; - if (isFlutterPackage(packageDir, fileSystem)) { - final List args = [ - 'test', - '--color', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ]; - - if (isWebPlugin(packageDir, fileSystem)) { - args.add('--platform=chrome'); - } - exitCode = await processRunner.runAndStream( - 'flutter', - args, - workingDir: packageDir, - ); - } else { - exitCode = await processRunner.runAndStream( - 'pub', - ['get'], - workingDir: packageDir, - ); - if (exitCode == 0) { - exitCode = await processRunner.runAndStream( - 'pub', - [ - 'run', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - 'test', - ], - workingDir: packageDir, - ); - } - } - if (exitCode != 0) { - failingPackages.add(packageName); - } - } - - print('\n\n'); - if (failingPackages.isNotEmpty) { - print('Tests for the following packages are failing (see above):'); - failingPackages.forEach((String package) { - print(' * $package'); - }); - throw ToolExit(1); - } - - print('All tests are passing!'); - } -} diff --git a/packages/fluttter_plugin_tools/lib/src/version_check_command.dart b/packages/fluttter_plugin_tools/lib/src/version_check_command.dart deleted file mode 100644 index 2c6b92bbcb7a..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/version_check_command.dart +++ /dev/null @@ -1,220 +0,0 @@ -// 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 'dart:io' as io; - -import 'package:meta/meta.dart'; -import 'package:colorize/colorize.dart'; -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; -import 'package:yaml/yaml.dart'; - -import 'common.dart'; - -const String _kBaseSha = 'base_sha'; - -class GitVersionFinder { - GitVersionFinder(this.baseGitDir, this.baseSha); - - final GitDir baseGitDir; - final String baseSha; - - static bool isPubspec(String file) { - return file.trim().endsWith('pubspec.yaml'); - } - - Future> getChangedPubSpecs() async { - final io.ProcessResult changedFilesCommand = await baseGitDir - .runCommand(['diff', '--name-only', '$baseSha', 'HEAD']); - final List changedFiles = - changedFilesCommand.stdout.toString().split('\n'); - return changedFiles.where(isPubspec).toList(); - } - - Future getPackageVersion(String pubspecPath, String gitRef) async { - final io.ProcessResult gitShow = - await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); - final String fileContent = gitShow.stdout; - final String versionString = loadYaml(fileContent)['version']; - return versionString == null ? null : Version.parse(versionString); - } -} - -enum NextVersionType { - BREAKING_MAJOR, - MAJOR_NULLSAFETY_PRE_RELEASE, - MINOR_NULLSAFETY_PRE_RELEASE, - MINOR, - PATCH, - RELEASE, -} - -Version getNextNullSafetyPreRelease(Version current, Version next) { - String nextNullsafetyPrerelease = 'nullsafety'; - if (current.isPreRelease && - current.preRelease.first is String && - current.preRelease.first == 'nullsafety') { - if (current.preRelease.length == 1) { - nextNullsafetyPrerelease = 'nullsafety.1'; - } else if (current.preRelease.length == 2 && - current.preRelease.last is int) { - nextNullsafetyPrerelease = 'nullsafety.${current.preRelease.last + 1}'; - } - } - return Version( - next.major, - next.minor, - next.patch, - pre: nextNullsafetyPrerelease, - ); -} - -@visibleForTesting -Map getAllowedNextVersions( - Version masterVersion, Version headVersion) { - final Version nextNullSafetyMajor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMajor); - final Version nextNullSafetyMinor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); - final Map allowedNextVersions = - { - masterVersion.nextMajor: NextVersionType.BREAKING_MAJOR, - nextNullSafetyMajor: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE, - nextNullSafetyMinor: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE, - masterVersion.nextMinor: NextVersionType.MINOR, - masterVersion.nextPatch: NextVersionType.PATCH, - }; - - if (masterVersion.major < 1 && headVersion.major < 1) { - int nextBuildNumber = -1; - if (masterVersion.build.isEmpty) { - nextBuildNumber = 1; - } else { - final int currentBuildNumber = masterVersion.build.first; - nextBuildNumber = currentBuildNumber + 1; - } - final Version preReleaseVersion = Version( - masterVersion.major, - masterVersion.minor, - masterVersion.patch, - build: nextBuildNumber.toString(), - ); - allowedNextVersions.clear(); - allowedNextVersions[masterVersion.nextMajor] = NextVersionType.RELEASE; - allowedNextVersions[masterVersion.nextMinor] = - NextVersionType.BREAKING_MAJOR; - allowedNextVersions[masterVersion.nextPatch] = NextVersionType.MINOR; - allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; - - final Version nextNullSafetyMajor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); - final Version nextNullSafetyMinor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextPatch); - - allowedNextVersions[nextNullSafetyMajor] = - NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE; - allowedNextVersions[nextNullSafetyMinor] = - NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE; - } - return allowedNextVersions; -} - -class VersionCheckCommand extends PluginCommand { - VersionCheckCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - this.gitDir, - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption(_kBaseSha); - } - - /// The git directory to use. By default it uses the parent directory. - /// - /// This can be mocked for testing. - final GitDir gitDir; - - @override - final String name = 'version-check'; - - @override - final String description = - 'Checks if the versions of the plugins have been incremented per pub specification.\n\n' - 'This command requires "pub" and "flutter" to be in your path.'; - - @override - Future run() async { - checkSharding(); - - final String rootDir = packagesDir.parent.absolute.path; - final String baseSha = argResults[_kBaseSha]; - - GitDir baseGitDir = gitDir; - if (baseGitDir == null) { - if (!await GitDir.isGitDir(rootDir)) { - print('$rootDir is not a valid Git repository.'); - throw ToolExit(2); - } - baseGitDir = await GitDir.fromExisting(rootDir); - } - - final GitVersionFinder gitVersionFinder = - GitVersionFinder(baseGitDir, baseSha); - - final List changedPubspecs = - await gitVersionFinder.getChangedPubSpecs(); - - for (final String pubspecPath in changedPubspecs) { - try { - final File pubspecFile = fileSystem.file(pubspecPath); - if (!pubspecFile.existsSync()) { - continue; - } - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); - if (pubspec.publishTo == 'none') { - continue; - } - - final Version masterVersion = - await gitVersionFinder.getPackageVersion(pubspecPath, baseSha); - final Version headVersion = - await gitVersionFinder.getPackageVersion(pubspecPath, 'HEAD'); - if (headVersion == null) { - continue; // Example apps don't have versions - } - - final Map allowedNextVersions = - getAllowedNextVersions(masterVersion, headVersion); - - if (!allowedNextVersions.containsKey(headVersion)) { - final String error = '$pubspecPath incorrectly updated version.\n' - 'HEAD: $headVersion, master: $masterVersion.\n' - 'Allowed versions: $allowedNextVersions'; - final Colorize redError = Colorize(error)..red(); - print(redError); - throw ToolExit(1); - } - - bool isPlatformInterface = pubspec.name.endsWith("_platform_interface"); - if (isPlatformInterface && - allowedNextVersions[headVersion] == - NextVersionType.BREAKING_MAJOR) { - final String error = '$pubspecPath breaking change detected.\n' - 'Breaking changes to platform interfaces are strongly discouraged.\n'; - final Colorize redError = Colorize(error)..red(); - print(redError); - throw ToolExit(1); - } - } on io.ProcessException { - print('Unable to find pubspec in master for $pubspecPath.' - ' Safe to ignore if the project is new.'); - } - } - - print('No version check errors found!'); - } -} diff --git a/packages/fluttter_plugin_tools/lib/src/xctest_command.dart b/packages/fluttter_plugin_tools/lib/src/xctest_command.dart deleted file mode 100644 index d90b7a8fbfea..000000000000 --- a/packages/fluttter_plugin_tools/lib/src/xctest_command.dart +++ /dev/null @@ -1,216 +0,0 @@ -// 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 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; - -import 'common.dart'; - -const String _kiOSDestination = 'ios-destination'; -const String _kTarget = 'target'; -const String _kSkip = 'skip'; -const String _kXcodeBuildCommand = 'xcodebuild'; -const String _kXCRunCommand = 'xcrun'; -const String _kFoundNoSimulatorsMessage = - 'Cannot find any available simulators, tests failed'; - -/// The command to run iOS' XCTests in plugins, this should work for both XCUnitTest and XCUITest targets. -/// The tests target have to be added to the xcode project of the example app. Usually at "example/ios/Runner.xcodeproj". -/// The command takes a "-target" argument which has to match the target of the test target. -/// For information on how to add test target in an xcode project, see https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/UnitTesting.html -class XCTestCommand extends PluginCommand { - XCTestCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption( - _kiOSDestination, - help: - 'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n' - 'this is passed to the `-destination` argument in xcodebuild command.\n' - 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', - ); - argParser.addOption(_kTarget, - help: 'The test target.\n' - 'This is the xcode project test target. This is passed to the `-scheme` argument in the xcodebuild command. \n' - 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the scheme'); - argParser.addMultiOption(_kSkip, - help: 'Plugins to skip while running this command. \n'); - } - - @override - final String name = 'xctest'; - - @override - final String description = 'Runs the xctests in the iOS example apps.\n\n' - 'This command requires "flutter" to be in your path.'; - - @override - Future run() async { - if (argResults[_kTarget] == null) { - // TODO(cyanglaz): Automatically find all the available testing schemes if this argument is not specified. - // https://github.com/flutter/flutter/issues/68419 - print('--$_kTarget must be specified'); - throw ToolExit(1); - } - - String destination = argResults[_kiOSDestination]; - if (destination == null) { - String simulatorId = await _findAvailableIphoneSimulator(); - if (simulatorId == null) { - print(_kFoundNoSimulatorsMessage); - throw ToolExit(1); - } - destination = 'id=$simulatorId'; - } - - checkSharding(); - - final String target = argResults[_kTarget]; - final List skipped = argResults[_kSkip]; - - List failingPackages = []; - await for (Directory plugin in getPlugins()) { - // Start running for package. - final String packageName = - p.relative(plugin.path, from: packagesDir.path); - print('Start running for $packageName ...'); - if (!isIosPlugin(plugin, fileSystem)) { - print('iOS is not supported by this plugin.'); - print('\n\n'); - continue; - } - if (skipped.contains(packageName)) { - print('$packageName was skipped with the --skip flag.'); - print('\n\n'); - continue; - } - for (Directory example in getExamplesForPlugin(plugin)) { - // Look for the test scheme in the example app. - print('Look for target named: $_kTarget ...'); - final List findSchemeArgs = [ - '-project', - 'ios/Runner.xcodeproj', - '-list', - '-json' - ]; - final String completeFindSchemeCommand = - '$_kXcodeBuildCommand ${findSchemeArgs.join(' ')}'; - print(completeFindSchemeCommand); - final io.ProcessResult xcodeprojListResult = await processRunner - .run(_kXcodeBuildCommand, findSchemeArgs, workingDir: example); - if (xcodeprojListResult.exitCode != 0) { - print('Error occurred while running "$completeFindSchemeCommand":\n' - '${xcodeprojListResult.stderr}'); - failingPackages.add(packageName); - print('\n\n'); - continue; - } - - final String xcodeprojListOutput = xcodeprojListResult.stdout; - Map xcodeprojListOutputJson = - jsonDecode(xcodeprojListOutput); - if (!xcodeprojListOutputJson['project']['targets'].contains(target)) { - failingPackages.add(packageName); - print('$target not configured for $packageName, test failed.'); - print( - 'Please check the scheme for the test target if it matches the name $target.\n' - 'If this plugin does not have an XCTest target, use the $_kSkip flag in the $name command to skip the plugin.'); - print('\n\n'); - continue; - } - // Found the scheme, running tests - print('Running XCTests:$target for $packageName ...'); - final List xctestArgs = [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - target, - '-destination', - destination, - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ]; - final String completeTestCommand = - '$_kXcodeBuildCommand ${xctestArgs.join(' ')}'; - print(completeTestCommand); - final int exitCode = await processRunner - .runAndStream(_kXcodeBuildCommand, xctestArgs, workingDir: example); - if (exitCode == 0) { - print('Successfully ran xctest for $packageName'); - } else { - failingPackages.add(packageName); - } - } - } - - // Command end, print reports. - if (failingPackages.isEmpty) { - print("All XCTests have passed!"); - } else { - print( - 'The following packages are failing XCTests (see above for details):'); - for (String package in failingPackages) { - print(' * $package'); - } - throw ToolExit(1); - } - } - - Future _findAvailableIphoneSimulator() async { - // Find the first available destination if not specified. - final List findSimulatorsArguments = [ - 'simctl', - 'list', - '--json' - ]; - final String findSimulatorCompleteCommand = - '$_kXCRunCommand ${findSimulatorsArguments.join(' ')}'; - print('Looking for available simulators...'); - print(findSimulatorCompleteCommand); - final io.ProcessResult findSimulatorsResult = - await processRunner.run(_kXCRunCommand, findSimulatorsArguments); - if (findSimulatorsResult.exitCode != 0) { - print('Error occurred while running "$findSimulatorCompleteCommand":\n' - '${findSimulatorsResult.stderr}'); - throw ToolExit(1); - } - final Map simulatorListJson = - jsonDecode(findSimulatorsResult.stdout); - final List runtimes = simulatorListJson['runtimes']; - final Map devices = simulatorListJson['devices']; - if (runtimes.isEmpty || devices.isEmpty) { - return null; - } - String id; - // Looking for runtimes, trying to find one with highest OS version. - for (Map runtimeMap in runtimes.reversed) { - if (!runtimeMap['name'].contains('iOS')) { - continue; - } - final String runtimeID = runtimeMap['identifier']; - final List devicesForRuntime = devices[runtimeID]; - if (devicesForRuntime.isEmpty) { - continue; - } - // Looking for runtimes, trying to find latest version of device. - for (Map device in devicesForRuntime.reversed) { - if (device['availabilityError'] != null || - (device['isAvailable'] as bool == false)) { - continue; - } - id = device['udid']; - print('device selected: $device'); - return id; - } - } - return null; - } -} diff --git a/packages/fluttter_plugin_tools/pubspec.yaml b/packages/fluttter_plugin_tools/pubspec.yaml deleted file mode 100644 index f8b452910236..000000000000 --- a/packages/fluttter_plugin_tools/pubspec.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: flutter_plugin_tools -description: Productivity utils for hosting multiple plugins within one repository. -homepage: https://github.com/flutter/plugin_tools -version: 0.0.45+1 - -dependencies: - args: "^1.4.3" - path: "^1.6.1" - http: "^0.12.1" - async: "^2.0.7" - yaml: "^2.1.15" - quiver: "^2.0.2" - pub_semver: ^1.4.2 - colorize: ^2.0.0 - git: ^1.0.0 - platform: ^2.2.0 - pubspec_parse: "^0.1.4" - test: ^1.6.4 - meta: ^1.1.7 - file: ^5.0.10 - uuid: ^2.0.4 - http_multi_server: ^2.2.0 - -dev_dependencies: - matcher: ^0.12.6 - mockito: ^4.1.1 - pedantic: 1.8.0 - -environment: - sdk: ">=2.3.0 <3.0.0" diff --git a/packages/fluttter_plugin_tools/test/analyze_command_test.dart b/packages/fluttter_plugin_tools/test/analyze_command_test.dart deleted file mode 100644 index 9e7a42bbb680..000000000000 --- a/packages/fluttter_plugin_tools/test/analyze_command_test.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/analyze_command.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - RecordingProcessRunner processRunner; - CommandRunner runner; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final AnalyzeCommand analyzeCommand = AnalyzeCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner('analyze_command', 'Test for analyze_command'); - runner.addCommand(analyzeCommand); - }); - - tearDown(() { - mockPackagesDir.deleteSync(recursive: true); - }); - - test('analyzes all packages', () async { - final Directory plugin1Dir = await createFakePlugin('a'); - final Directory plugin2Dir = await createFakePlugin('b'); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - await runner.run(['analyze']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('pub', ['global', 'activate', 'tuneup'], - mockPackagesDir.path), - ProcessCall('flutter', ['packages', 'get'], plugin1Dir.path), - ProcessCall('flutter', ['packages', 'get'], plugin2Dir.path), - ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], - plugin1Dir.path), - ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], - plugin2Dir.path), - ])); - }); - - group('verifies analysis settings', () { - test('fails analysis_options.yaml', () async { - await createFakePlugin('foo', withExtraFiles: >[ - ['analysis_options.yaml'] - ]); - - await expectLater(() => runner.run(['analyze']), - throwsA(const TypeMatcher())); - }); - - test('fails .analysis_options', () async { - await createFakePlugin('foo', withExtraFiles: >[ - ['.analysis_options'] - ]); - - await expectLater(() => runner.run(['analyze']), - throwsA(const TypeMatcher())); - }); - - test('takes an allow list', () async { - final Directory pluginDir = - await createFakePlugin('foo', withExtraFiles: >[ - ['analysis_options.yaml'] - ]); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - await runner.run(['analyze', '--custom-analysis', 'foo']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('pub', ['global', 'activate', 'tuneup'], - mockPackagesDir.path), - ProcessCall('flutter', ['packages', 'get'], pluginDir.path), - ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], - pluginDir.path), - ])); - }); - }); -} diff --git a/packages/fluttter_plugin_tools/test/build_examples_command_test.dart b/packages/fluttter_plugin_tools/test/build_examples_command_test.dart deleted file mode 100644 index eaf5049dcc02..000000000000 --- a/packages/fluttter_plugin_tools/test/build_examples_command_test.dart +++ /dev/null @@ -1,470 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/build_examples_command.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - group('test build_example_command', () { - CommandRunner runner; - RecordingProcessRunner processRunner; - final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final BuildExamplesCommand command = BuildExamplesCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner( - 'build_examples_command', 'Test for build_example_command'); - runner.addCommand(command); - cleanupPackages(); - }); - - test('building for iOS when plugin is not set up for iOS results in no-op', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--ipa', '--no-macos']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING IPA for $packageName', - 'iOS is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - - test('building for ios', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'build-examples', - '--ipa', - '--no-macos', - '--enable-experiment=exp1' - ]); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING IPA for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'build', - 'ios', - '--no-codesign', - '--enable-experiment=exp1' - ], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test( - 'building for Linux when plugin is not set up for Linux results in no-op', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--linux']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING Linux for $packageName', - 'Linux is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --linux with no - // Linux implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - - test('building for Linux', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--linux']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING Linux for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(flutterCommand, ['build', 'linux'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test('building for macos with no implementation results in no-op', - () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['example', 'test'], - ]); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--macos']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING macOS for $packageName', - '\macOS is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - test('building for macos', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ['example', 'macos', 'macos.swift'], - ], - isMacOsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--macos']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING macOS for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(flutterCommand, ['pub', 'get'], - pluginExampleDirectory.path), - ProcessCall(flutterCommand, ['build', 'macos'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test( - 'building for Windows when plugin is not set up for Windows results in no-op', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isWindowsPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--windows']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING Windows for $packageName', - 'Windows is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - - test('building for windows', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isWindowsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--windows']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING Windows for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(flutterCommand, ['build', 'windows'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test( - 'building for Android when plugin is not set up for Android results in no-op', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--apk', '--no-ipa']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING APK for $packageName', - 'Android is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - - test('building for android', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isAndroidPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'build-examples', - '--apk', - '--no-ipa', - '--no-macos', - ]); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING APK for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(flutterCommand, ['build', 'apk'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test('enable-experiment flag for Android', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isAndroidPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - await runCapturingPrint(runner, [ - 'build-examples', - '--apk', - '--no-ipa', - '--no-macos', - '--enable-experiment=exp1' - ]); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - ['build', 'apk', '--enable-experiment=exp1'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test('enable-experiment flag for ios', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - await runCapturingPrint(runner, [ - 'build-examples', - '--ipa', - '--no-macos', - '--enable-experiment=exp1' - ]); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'build', - 'ios', - '--no-codesign', - '--enable-experiment=exp1' - ], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - }); -} diff --git a/packages/fluttter_plugin_tools/test/common_test.dart b/packages/fluttter_plugin_tools/test/common_test.dart deleted file mode 100644 index b3504c2358d9..000000000000 --- a/packages/fluttter_plugin_tools/test/common_test.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - RecordingProcessRunner processRunner; - CommandRunner runner; - List plugins; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - plugins = []; - final SamplePluginCommand samplePluginCommand = SamplePluginCommand( - plugins, - mockPackagesDir, - mockFileSystem, - processRunner: processRunner, - ); - runner = - CommandRunner('common_command', 'Test for common functionality'); - runner.addCommand(samplePluginCommand); - }); - - tearDown(() { - mockPackagesDir.deleteSync(recursive: true); - }); - - test('all plugins from file system', () async { - final Directory plugin1 = createFakePlugin('plugin1'); - final Directory plugin2 = createFakePlugin('plugin2'); - await runner.run(['sample']); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); - }); - - test('exclude plugins when plugins flag is specified', () async { - createFakePlugin('plugin1'); - final Directory plugin2 = createFakePlugin('plugin2'); - await runner.run( - ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); - expect(plugins, unorderedEquals([plugin2.path])); - }); - - test('exclude plugins when plugins flag isn\'t specified', () async { - createFakePlugin('plugin1'); - createFakePlugin('plugin2'); - await runner.run(['sample', '--exclude=plugin1,plugin2']); - expect(plugins, unorderedEquals([])); - }); - - test('exclude federated plugins when plugins flag is specified', () async { - createFakePlugin('plugin1', parentDirectoryName: 'federated'); - final Directory plugin2 = createFakePlugin('plugin2'); - await runner.run([ - 'sample', - '--plugins=federated/plugin1,plugin2', - '--exclude=federated/plugin1' - ]); - expect(plugins, unorderedEquals([plugin2.path])); - }); - - test('exclude entire federated plugins when plugins flag is specified', - () async { - createFakePlugin('plugin1', parentDirectoryName: 'federated'); - final Directory plugin2 = createFakePlugin('plugin2'); - await runner.run([ - 'sample', - '--plugins=federated/plugin1,plugin2', - '--exclude=federated' - ]); - expect(plugins, unorderedEquals([plugin2.path])); - }); -} - -class SamplePluginCommand extends PluginCommand { - SamplePluginCommand( - this.plugins_, - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner); - - List plugins_; - - @override - final String name = 'sample'; - - @override - final String description = 'sample command'; - - @override - Future run() async { - await for (Directory package in getPlugins()) { - this.plugins_.add(package.path); - } - } -} diff --git a/packages/fluttter_plugin_tools/test/drive_examples_command_test.dart b/packages/fluttter_plugin_tools/test/drive_examples_command_test.dart deleted file mode 100644 index f4bdd95c1664..000000000000 --- a/packages/fluttter_plugin_tools/test/drive_examples_command_test.dart +++ /dev/null @@ -1,505 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - group('test drive_example_command', () { - CommandRunner runner; - RecordingProcessRunner processRunner; - final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final DriveExamplesCommand command = DriveExamplesCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner( - 'drive_examples_command', 'Test for drive_example_command'); - runner.addCommand(command); - }); - - tearDown(() { - cleanupPackages(); - }); - - test('driving under folder "test"', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test', 'plugin.dart'], - ], - isIosPlugin: true, - isAndroidPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving under folder "test_driver"', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isAndroidPlugin: true, - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving under folder "test_driver" when test files are missing"', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ], - isAndroidPlugin: true, - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - await expectLater( - () => runCapturingPrint(runner, ['drive-examples']), - throwsA(const TypeMatcher())); - }); - - test( - 'driving under folder "test_driver" when targets are under "integration_test"', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'integration_test.dart'], - ['example', 'integration_test', 'bar_test.dart'], - ['example', 'integration_test', 'foo_test.dart'], - ['example', 'integration_test', 'ignore_me.dart'], - ], - isAndroidPlugin: true, - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String driverTestPath = p.join('test_driver', 'integration_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - p.join('integration_test', 'bar_test.dart'), - ], - pluginExampleDirectory.path), - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - p.join('integration_test', 'foo_test.dart'), - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not support Linux is a no-op', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isMacOsPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--linux', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running drive-examples --linux on a non-Linux - // plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving on a Linux plugin', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isLinuxPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--linux', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '-d', - 'linux', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not suppport macOS is a no-op', () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ]); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--macos', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running drive-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, []); - }); - test('driving on a macOS plugin', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ['example', 'macos', 'macos.swift'], - ], - isMacOsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--macos', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '-d', - 'macos', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not suppport windows is a no-op', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isMacOsPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--windows', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running drive-examples --windows on a non-windows - // plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving on a Windows plugin', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isWindowsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--windows', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '-d', - 'windows', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not support mobile is no-op', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isMacOsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running drive-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('enable-experiment flag', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test', 'plugin.dart'], - ], - isIosPlugin: true, - isAndroidPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - await runCapturingPrint(runner, [ - 'drive-examples', - '--enable-experiment=exp1', - ]); - - String deviceTestPath = p.join('test', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--enable-experiment=exp1', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - }); -} diff --git a/packages/fluttter_plugin_tools/test/firebase_test_lab_test.dart b/packages/fluttter_plugin_tools/test/firebase_test_lab_test.dart deleted file mode 100644 index 97b977619d57..000000000000 --- a/packages/fluttter_plugin_tools/test/firebase_test_lab_test.dart +++ /dev/null @@ -1,256 +0,0 @@ -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$FirebaseTestLabCommand', () { - final List printedMessages = []; - CommandRunner runner; - RecordingProcessRunner processRunner; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final FirebaseTestLabCommand command = FirebaseTestLabCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner, - print: (Object message) => printedMessages.add(message.toString())); - - runner = CommandRunner( - 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); - runner.addCommand(command); - }); - - tearDown(() { - printedMessages.clear(); - }); - - test('retries gcloud set', () async { - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(1); - processRunner.processToReturn = mockProcess; - createFakePlugin('plugin', withExtraFiles: >[ - ['lib/test/should_not_run_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e_test.dart'], - ['example', 'android', 'gradlew'], - ['example', 'should_not_run_e2e.dart'], - [ - 'example', - 'android', - 'app', - 'src', - 'androidTest', - 'MainActivityTest.java' - ], - ]); - await expectLater( - () => runCapturingPrint(runner, ['firebase-test-lab']), - throwsA(const TypeMatcher())); - expect( - printedMessages, - contains( - "\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.")); - }); - - test('runs e2e tests', () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['test', 'plugin_test.dart'], - ['test', 'plugin_e2e.dart'], - ['should_not_run_e2e.dart'], - ['lib/test/should_not_run_e2e.dart'], - ['example', 'test', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e_test.dart'], - ['example', 'integration_test', 'foo_test.dart'], - ['example', 'integration_test', 'should_not_run.dart'], - ['example', 'android', 'gradlew'], - ['example', 'should_not_run_e2e.dart'], - [ - 'example', - 'android', - 'app', - 'src', - 'androidTest', - 'MainActivityTest.java' - ], - ]); - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=flame,version=29', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - ]); - - expect( - printedMessages, - orderedEquals([ - '\nRUNNING FIREBASE TEST LAB TESTS for plugin', - '\nFirebase project configured.', - '\n\n', - 'All Firebase Test Lab tests successful!', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true'.split(' '), - '/packages/plugin/example/android'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ]), - ); - }); - - test('experimental flag', () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['test', 'plugin_test.dart'], - ['test', 'plugin_e2e.dart'], - ['should_not_run_e2e.dart'], - ['lib/test/should_not_run_e2e.dart'], - ['example', 'test', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e_test.dart'], - ['example', 'integration_test', 'foo_test.dart'], - ['example', 'integration_test', 'should_not_run.dart'], - ['example', 'android', 'gradlew'], - ['example', 'should_not_run_e2e.dart'], - [ - 'example', - 'android', - 'app', - 'src', - 'androidTest', - 'MainActivityTest.java' - ], - ]); - - await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=flame,version=29', - '--test-run-id', - 'testRunId', - '--enable-experiment=exp1', - ]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ]), - ); - - cleanupPackages(); - }); - }); -} diff --git a/packages/fluttter_plugin_tools/test/lint_podspecs_command_test.dart b/packages/fluttter_plugin_tools/test/lint_podspecs_command_test.dart deleted file mode 100644 index 49d6ad4d8e20..000000000000 --- a/packages/fluttter_plugin_tools/test/lint_podspecs_command_test.dart +++ /dev/null @@ -1,202 +0,0 @@ -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$LintPodspecsCommand', () { - CommandRunner runner; - MockPlatform mockPlatform; - final RecordingProcessRunner processRunner = RecordingProcessRunner(); - List printedMessages; - - setUp(() { - initializeFakePackages(); - - printedMessages = []; - mockPlatform = MockPlatform(); - when(mockPlatform.isMacOS).thenReturn(true); - final LintPodspecsCommand command = LintPodspecsCommand( - mockPackagesDir, - mockFileSystem, - processRunner: processRunner, - platform: mockPlatform, - print: (Object message) => printedMessages.add(message.toString()), - ); - - runner = - CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); - runner.addCommand(command); - final MockProcess mockLintProcess = MockProcess(); - mockLintProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockLintProcess; - processRunner.recordedCalls.clear(); - }); - - tearDown(() { - cleanupPackages(); - }); - - test('only runs on macOS', () async { - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'], - ]); - - when(mockPlatform.isMacOS).thenReturn(false); - await runner.run(['podspecs']); - - expect( - processRunner.recordedCalls, - equals([]), - ); - }); - - test('runs pod lib lint on a podspec', () async { - Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['ios', 'plugin1.podspec'], - ['bogus.dart'], // Ignore non-podspecs. - ]); - - processRunner.resultStdout = 'Foo'; - processRunner.resultStderr = 'Bar'; - - await runner.run(['podspecs']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), - '--analyze', - '--use-libraries' - ], - mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), - '--analyze', - ], - mockPackagesDir.path), - ]), - ); - - expect( - printedMessages, contains('Linting and analyzing plugin1.podspec')); - expect(printedMessages, contains('Foo')); - expect(printedMessages, contains('Bar')); - }); - - test('skips podspecs with known issues', () async { - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'] - ]); - createFakePlugin('plugin2', withExtraFiles: >[ - ['plugin2.podspec'] - ]); - - await runner - .run(['podspecs', '--skip=plugin1', '--skip=plugin2']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), - ]), - ); - }); - - test('skips analyzer for podspecs with known warnings', () async { - Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'], - ]); - - await runner.run(['podspecs', '--no-analyze=plugin1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - '--use-libraries' - ], - mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - ], - mockPackagesDir.path), - ]), - ); - - expect(printedMessages, contains('Linting plugin1.podspec')); - }); - - test('allow warnings for podspecs with known warnings', () async { - Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'], - ]); - - await runner.run(['podspecs', '--ignore-warnings=plugin1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - '--allow-warnings', - '--analyze', - '--use-libraries' - ], - mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - '--allow-warnings', - '--analyze', - ], - mockPackagesDir.path), - ]), - ); - - expect( - printedMessages, contains('Linting and analyzing plugin1.podspec')); - }); - }); -} - -class MockPlatform extends Mock implements Platform {} diff --git a/packages/fluttter_plugin_tools/test/list_command_test.dart b/packages/fluttter_plugin_tools/test/list_command_test.dart deleted file mode 100644 index 478625283dd0..000000000000 --- a/packages/fluttter_plugin_tools/test/list_command_test.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/list_command.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - group('$ListCommand', () { - CommandRunner runner; - - setUp(() { - initializeFakePackages(); - final ListCommand command = ListCommand(mockPackagesDir, mockFileSystem); - - runner = CommandRunner('list_test', 'Test for $ListCommand'); - runner.addCommand(command); - }); - - test('lists plugins', () async { - createFakePlugin('plugin1'); - createFakePlugin('plugin2'); - - final List plugins = - await runCapturingPrint(runner, ['list', '--type=plugin']); - - expect( - plugins, - orderedEquals([ - '/packages/plugin1', - '/packages/plugin2', - ]), - ); - - cleanupPackages(); - }); - - test('lists examples', () async { - createFakePlugin('plugin1', withSingleExample: true); - createFakePlugin('plugin2', - withExamples: ['example1', 'example2']); - createFakePlugin('plugin3'); - - final List examples = - await runCapturingPrint(runner, ['list', '--type=example']); - - expect( - examples, - orderedEquals([ - '/packages/plugin1/example', - '/packages/plugin2/example/example1', - '/packages/plugin2/example/example2', - ]), - ); - - cleanupPackages(); - }); - - test('lists packages', () async { - createFakePlugin('plugin1', withSingleExample: true); - createFakePlugin('plugin2', - withExamples: ['example1', 'example2']); - createFakePlugin('plugin3'); - - final List packages = - await runCapturingPrint(runner, ['list', '--type=package']); - - expect( - packages, - unorderedEquals([ - '/packages/plugin1', - '/packages/plugin1/example', - '/packages/plugin2', - '/packages/plugin2/example/example1', - '/packages/plugin2/example/example2', - '/packages/plugin3', - ]), - ); - - cleanupPackages(); - }); - - test('lists files', () async { - createFakePlugin('plugin1', withSingleExample: true); - createFakePlugin('plugin2', - withExamples: ['example1', 'example2']); - createFakePlugin('plugin3'); - - final List examples = - await runCapturingPrint(runner, ['list', '--type=file']); - - expect( - examples, - unorderedEquals([ - '/packages/plugin1/pubspec.yaml', - '/packages/plugin1/example/pubspec.yaml', - '/packages/plugin2/pubspec.yaml', - '/packages/plugin2/example/example1/pubspec.yaml', - '/packages/plugin2/example/example2/pubspec.yaml', - '/packages/plugin3/pubspec.yaml', - ]), - ); - - cleanupPackages(); - }); - - test('lists plugins using federated plugin layout', () async { - createFakePlugin('plugin1'); - - // Create a federated plugin by creating a directory under the packages - // directory with several packages underneath. - final Directory federatedPlugin = - mockPackagesDir.childDirectory('my_plugin')..createSync(); - final Directory clientLibrary = - federatedPlugin.childDirectory('my_plugin')..createSync(); - createFakePubspec(clientLibrary); - final Directory webLibrary = - federatedPlugin.childDirectory('my_plugin_web')..createSync(); - createFakePubspec(webLibrary); - final Directory macLibrary = - federatedPlugin.childDirectory('my_plugin_macos')..createSync(); - createFakePubspec(macLibrary); - - // Test without specifying `--type`. - final List plugins = - await runCapturingPrint(runner, ['list']); - - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - '/packages/my_plugin/my_plugin', - '/packages/my_plugin/my_plugin_web', - '/packages/my_plugin/my_plugin_macos', - ]), - ); - - cleanupPackages(); - }); - - test('can filter plugins with the --plugins argument', () async { - createFakePlugin('plugin1'); - - // Create a federated plugin by creating a directory under the packages - // directory with several packages underneath. - final Directory federatedPlugin = - mockPackagesDir.childDirectory('my_plugin')..createSync(); - final Directory clientLibrary = - federatedPlugin.childDirectory('my_plugin')..createSync(); - createFakePubspec(clientLibrary); - final Directory webLibrary = - federatedPlugin.childDirectory('my_plugin_web')..createSync(); - createFakePubspec(webLibrary); - final Directory macLibrary = - federatedPlugin.childDirectory('my_plugin_macos')..createSync(); - createFakePubspec(macLibrary); - - List plugins = await runCapturingPrint( - runner, ['list', '--plugins=plugin1']); - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - ]), - ); - - plugins = await runCapturingPrint( - runner, ['list', '--plugins=my_plugin']); - expect( - plugins, - unorderedEquals([ - '/packages/my_plugin/my_plugin', - '/packages/my_plugin/my_plugin_web', - '/packages/my_plugin/my_plugin_macos', - ]), - ); - - plugins = await runCapturingPrint( - runner, ['list', '--plugins=my_plugin/my_plugin_web']); - expect( - plugins, - unorderedEquals([ - '/packages/my_plugin/my_plugin_web', - ]), - ); - - plugins = await runCapturingPrint(runner, - ['list', '--plugins=my_plugin/my_plugin_web,plugin1']); - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - '/packages/my_plugin/my_plugin_web', - ]), - ); - }); - }); -} diff --git a/packages/fluttter_plugin_tools/test/mocks.dart b/packages/fluttter_plugin_tools/test/mocks.dart deleted file mode 100644 index 3e17ff8efd32..000000000000 --- a/packages/fluttter_plugin_tools/test/mocks.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:async'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:mockito/mockito.dart'; - -class MockProcess extends Mock implements io.Process { - final Completer exitCodeCompleter = Completer(); - final StreamController> stdoutController = - StreamController>(); - final StreamController> stderrController = - StreamController>(); - final MockIOSink stdinMock = MockIOSink(); - - @override - Future get exitCode => exitCodeCompleter.future; - - @override - Stream> get stdout => stdoutController.stream; - - @override - Stream> get stderr => stderrController.stream; - - @override - IOSink get stdin => stdinMock; -} - -class MockIOSink extends Mock implements IOSink { - List lines = []; - - @override - void writeln([Object obj = ""]) => lines.add(obj); -} diff --git a/packages/fluttter_plugin_tools/test/publish_plugin_command_test.dart b/packages/fluttter_plugin_tools/test/publish_plugin_command_test.dart deleted file mode 100644 index d01bfa67ce24..000000000000 --- a/packages/fluttter_plugin_tools/test/publish_plugin_command_test.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:git/git.dart'; -import 'package:matcher/matcher.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - const String testPluginName = 'foo'; - final List printedMessages = []; - - Directory parentDir; - Directory pluginDir; - GitDir gitDir; - TestProcessRunner processRunner; - CommandRunner commandRunner; - MockStdin mockStdin; - - setUp(() async { - // This test uses a local file system instead of an in memory one throughout - // so that git actually works. In setup we initialize a mono repo of plugins - // with one package and commit everything to Git. - parentDir = const LocalFileSystem() - .systemTempDirectory - .createTempSync('publish_plugin_command_test-'); - initializeFakePackages(parentDir: parentDir); - pluginDir = createFakePlugin(testPluginName, withSingleExample: false); - assert(pluginDir != null && pluginDir.existsSync()); - createFakePubspec(pluginDir, includeVersion: true); - io.Process.runSync('git', ['init'], - workingDirectory: mockPackagesDir.path); - gitDir = await GitDir.fromExisting(mockPackagesDir.path); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Initial commit']); - processRunner = TestProcessRunner(); - mockStdin = MockStdin(); - commandRunner = CommandRunner('tester', '') - ..addCommand(PublishPluginCommand( - mockPackagesDir, const LocalFileSystem(), - processRunner: processRunner, - print: (Object message) => printedMessages.add(message.toString()), - stdinput: mockStdin)); - }); - - tearDown(() { - parentDir.deleteSync(recursive: true); - printedMessages.clear(); - }); - - group('Initial validation', () { - test('requires a package flag', () async { - await expectLater(() => commandRunner.run(['publish-plugin']), - throwsA(const TypeMatcher())); - - expect( - printedMessages.last, contains("Must specify a package to publish.")); - }); - - test('requires an existing flag', () async { - await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', 'iamerror']), - throwsA(const TypeMatcher())); - - expect(printedMessages.last, contains('iamerror does not exist')); - }); - - test('refuses to proceed with dirty files', () async { - pluginDir.childFile('tmp').createSync(); - - await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', testPluginName]), - throwsA(const TypeMatcher())); - - expect( - printedMessages.last, - contains( - "There are files in the package directory that haven't been saved in git.")); - }); - - test('fails immediately if the remote doesn\'t exist', () async { - await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', testPluginName]), - throwsA(const TypeMatcher())); - - expect(processRunner.results.last.stderr, contains("No such remote")); - }); - - test("doesn't validate the remote if it's not pushing tags", () async { - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release' - ]); - - expect(printedMessages.last, 'Done!'); - }); - }); - - group('Publishes package', () { - test('while showing all output from pub publish to the user', () async { - final Future publishCommand = commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release' - ]); - processRunner.mockPublishProcess.stdoutController.add(utf8.encode('Foo')); - processRunner.mockPublishProcess.stderrController.add(utf8.encode('Bar')); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - - await publishCommand; - - expect(printedMessages, contains('Foo')); - expect(printedMessages, contains('Bar')); - }); - - test('forwards input from the user to `pub publish`', () async { - final Future publishCommand = commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release' - ]); - mockStdin.controller.add(utf8.encode('user input')); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - - await publishCommand; - - expect(processRunner.mockPublishProcess.stdinMock.lines, - contains('user input')); - }); - - test('forwards --pub-publish-flags to pub publish', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release', - '--pub-publish-flags', - '--dry-run,--server=foo' - ]); - - expect(processRunner.mockPublishArgs.length, 4); - expect(processRunner.mockPublishArgs[0], 'pub'); - expect(processRunner.mockPublishArgs[1], 'publish'); - expect(processRunner.mockPublishArgs[2], '--dry-run'); - expect(processRunner.mockPublishArgs[3], '--server=foo'); - }); - - test('throws if pub publish fails', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(128); - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release', - ]), - throwsA(const TypeMatcher())); - - expect(printedMessages, contains("Publish failed. Exiting.")); - }); - }); - - group('Tags release', () { - test('with the version and name from the pubspec.yaml', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - ]); - - final String tag = - (await gitDir.runCommand(['show-ref', 'fake_package-v0.0.1'])) - .stdout; - expect(tag, isNotEmpty); - }); - - test('only if publishing succeeded', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(128); - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - ]), - throwsA(const TypeMatcher())); - - expect(printedMessages, contains("Publish failed. Exiting.")); - final String tag = (await gitDir.runCommand( - ['show-ref', 'fake_package-v0.0.1'], - throwOnError: false)) - .stdout; - expect(tag, isEmpty); - }); - }); - - group('Pushes tags', () { - setUp(() async { - await gitDir.runCommand( - ['remote', 'add', 'upstream', 'http://localhost:8000']); - }); - - test('requires user confirmation', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - mockStdin.readLineOutput = 'help'; - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - ]), - throwsA(const TypeMatcher())); - - expect(printedMessages, contains('Tag push canceled.')); - }); - - test('to upstream by default', () async { - await gitDir.runCommand(['tag', 'garbage']); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - mockStdin.readLineOutput = 'y'; - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - ]); - - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); - expect(printedMessages.last, 'Done!'); - }); - - test('to different remotes based on a flag', () async { - await gitDir.runCommand( - ['remote', 'add', 'origin', 'http://localhost:8001']); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - mockStdin.readLineOutput = 'y'; - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--remote', - 'origin', - ]); - - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'origin'); - expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); - expect(printedMessages.last, 'Done!'); - }); - - test('only if tagging and pushing to remotes are both enabled', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-tag-release', - ]); - - expect(processRunner.pushTagsArgs.isEmpty, isTrue); - expect(printedMessages.last, 'Done!'); - }); - }); -} - -class TestProcessRunner extends ProcessRunner { - final List results = []; - final MockProcess mockPublishProcess = MockProcess(); - final List mockPublishArgs = []; - final MockProcessResult mockPushTagsResult = MockProcessResult(); - final List pushTagsArgs = []; - - @override - Future runAndExitOnError( - String executable, - List args, { - Directory workingDir, - }) async { - // Don't ever really push tags. - if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { - pushTagsArgs.addAll(args); - return mockPushTagsResult; - } - - final io.ProcessResult result = io.Process.runSync(executable, args, - workingDirectory: workingDir?.path); - results.add(result); - if (result.exitCode != 0) { - throw ToolExit(result.exitCode); - } - return result; - } - - @override - Future start(String executable, List args, - {Directory workingDirectory}) async { - /// Never actually publish anything. Start is always and only used for this - /// since it returns something we can route stdin through. - assert(executable == 'flutter' && - args.isNotEmpty && - args[0] == 'pub' && - args[1] == 'publish'); - mockPublishArgs.addAll(args); - return mockPublishProcess; - } -} - -class MockStdin extends Mock implements io.Stdin { - final StreamController> controller = StreamController>(); - String readLineOutput; - - @override - Stream transform(StreamTransformer streamTransformer) { - return controller.stream.transform(streamTransformer); - } - - @override - StreamSubscription> listen(void onData(List event), - {Function onError, void onDone(), bool cancelOnError}) { - return controller.stream.listen(onData, - onError: onError, onDone: onDone, cancelOnError: cancelOnError); - } - - @override - String readLineSync( - {Encoding encoding = io.systemEncoding, - bool retainNewlines = false}) => - readLineOutput; -} - -class MockProcessResult extends Mock implements io.ProcessResult {} diff --git a/packages/fluttter_plugin_tools/test/test_command_test.dart b/packages/fluttter_plugin_tools/test/test_command_test.dart deleted file mode 100644 index 514e4c27190a..000000000000 --- a/packages/fluttter_plugin_tools/test/test_command_test.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/test_command.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - group('$TestCommand', () { - CommandRunner runner; - final RecordingProcessRunner processRunner = RecordingProcessRunner(); - - setUp(() { - initializeFakePackages(); - final TestCommand command = TestCommand(mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner('test_test', 'Test for $TestCommand'); - runner.addCommand(command); - }); - - tearDown(() { - cleanupPackages(); - processRunner.recordedCalls.clear(); - }); - - test('runs flutter test on each plugin', () async { - final Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = - createFakePlugin('plugin2', withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - - await runner.run(['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', ['test', '--color'], plugin1Dir.path), - ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), - ]), - ); - - cleanupPackages(); - }); - - test('skips testing plugins without test directory', () async { - createFakePlugin('plugin1'); - final Directory plugin2Dir = - createFakePlugin('plugin2', withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - - await runner.run(['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), - ]), - ); - - cleanupPackages(); - }); - - test('runs pub run test on non-Flutter packages', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', - isFlutter: true, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = createFakePlugin('plugin2', - isFlutter: false, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - - await runner.run(['test', '--enable-experiment=exp1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - ['test', '--color', '--enable-experiment=exp1'], - plugin1Dir.path), - ProcessCall('pub', ['get'], plugin2Dir.path), - ProcessCall( - 'pub', - ['run', '--enable-experiment=exp1', 'test'], - plugin2Dir.path), - ]), - ); - - cleanupPackages(); - }); - - test('runs on Chrome for web plugins', () async { - final Directory pluginDir = createFakePlugin( - 'plugin', - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ], - isFlutter: true, - isWebPlugin: true, - ); - - await runner.run(['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', - ['test', '--color', '--platform=chrome'], pluginDir.path), - ]), - ); - }); - - test('enable-experiment flag', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', - isFlutter: true, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = createFakePlugin('plugin2', - isFlutter: false, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - - await runner.run(['test', '--enable-experiment=exp1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - ['test', '--color', '--enable-experiment=exp1'], - plugin1Dir.path), - ProcessCall('pub', ['get'], plugin2Dir.path), - ProcessCall( - 'pub', - ['run', '--enable-experiment=exp1', 'test'], - plugin2Dir.path), - ]), - ); - - cleanupPackages(); - }); - }); -} diff --git a/packages/fluttter_plugin_tools/test/util.dart b/packages/fluttter_plugin_tools/test/util.dart deleted file mode 100644 index ec0000d13f34..000000000000 --- a/packages/fluttter_plugin_tools/test/util.dart +++ /dev/null @@ -1,291 +0,0 @@ -import 'dart:async'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:platform/platform.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:quiver/collection.dart'; - -FileSystem mockFileSystem = MemoryFileSystem( - style: LocalPlatform().isWindows - ? FileSystemStyle.windows - : FileSystemStyle.posix); -Directory mockPackagesDir; - -/// Creates a mock packages directory in the mock file system. -/// -/// If [parentDir] is set the mock packages dir will be creates as a child of -/// it. If not [mockFileSystem] will be used instead. -void initializeFakePackages({Directory parentDir}) { - mockPackagesDir = - (parentDir ?? mockFileSystem.currentDirectory).childDirectory('packages'); - mockPackagesDir.createSync(); -} - -/// Creates a plugin package with the given [name] in [mockPackagesDir]. -Directory createFakePlugin( - String name, { - bool withSingleExample = false, - List withExamples = const [], - List> withExtraFiles = const >[], - bool isFlutter = true, - bool isAndroidPlugin = false, - bool isIosPlugin = false, - bool isWebPlugin = false, - bool isLinuxPlugin = false, - bool isMacOsPlugin = false, - bool isWindowsPlugin = false, - String parentDirectoryName = '', -}) { - assert(!(withSingleExample && withExamples.isNotEmpty), - 'cannot pass withSingleExample and withExamples simultaneously'); - - final Directory pluginDirectory = (parentDirectoryName != '') - ? mockPackagesDir.childDirectory(parentDirectoryName).childDirectory(name) - : mockPackagesDir.childDirectory(name); - pluginDirectory.createSync(recursive: true); - - createFakePubspec( - pluginDirectory, - name: name, - isFlutter: isFlutter, - isAndroidPlugin: isAndroidPlugin, - isIosPlugin: isIosPlugin, - isWebPlugin: isWebPlugin, - isLinuxPlugin: isLinuxPlugin, - isMacOsPlugin: isMacOsPlugin, - isWindowsPlugin: isWindowsPlugin, - ); - - if (withSingleExample) { - final Directory exampleDir = pluginDirectory.childDirectory('example') - ..createSync(); - createFakePubspec(exampleDir, - name: "${name}_example", isFlutter: isFlutter); - } else if (withExamples.isNotEmpty) { - final Directory exampleDir = pluginDirectory.childDirectory('example') - ..createSync(); - for (String example in withExamples) { - final Directory currentExample = exampleDir.childDirectory(example) - ..createSync(); - createFakePubspec(currentExample, name: example, isFlutter: isFlutter); - } - } - - for (List file in withExtraFiles) { - final List newFilePath = [pluginDirectory.path] - ..addAll(file); - final File newFile = - mockFileSystem.file(mockFileSystem.path.joinAll(newFilePath)); - newFile.createSync(recursive: true); - } - - return pluginDirectory; -} - -/// Creates a `pubspec.yaml` file with a flutter dependency. -void createFakePubspec( - Directory parent, { - String name = 'fake_package', - bool isFlutter = true, - bool includeVersion = false, - bool isAndroidPlugin = false, - bool isIosPlugin = false, - bool isWebPlugin = false, - bool isLinuxPlugin = false, - bool isMacOsPlugin = false, - bool isWindowsPlugin = false, -}) { - parent.childFile('pubspec.yaml').createSync(); - String yaml = ''' -name: $name -flutter: - plugin: - platforms: -'''; - if (isAndroidPlugin) { - yaml += ''' - android: - package: io.flutter.plugins.fake - pluginClass: FakePlugin -'''; - } - if (isIosPlugin) { - yaml += ''' - ios: - pluginClass: FLTFakePlugin -'''; - } - if (isWebPlugin) { - yaml += ''' - web: - pluginClass: FakePlugin - fileName: ${name}_web.dart -'''; - } - if (isLinuxPlugin) { - yaml += ''' - linux: - pluginClass: FakePlugin -'''; - } - if (isMacOsPlugin) { - yaml += ''' - macos: - pluginClass: FakePlugin -'''; - } - if (isWindowsPlugin) { - yaml += ''' - windows: - pluginClass: FakePlugin -'''; - } - if (isFlutter) { - yaml += ''' -dependencies: - flutter: - sdk: flutter -'''; - } - if (includeVersion) { - yaml += ''' -version: 0.0.1 -publish_to: none # Hardcoded safeguard to prevent this from somehow being published by a broken test. -'''; - } - parent.childFile('pubspec.yaml').writeAsStringSync(yaml); -} - -/// Cleans up the mock packages directory, making it an empty directory again. -void cleanupPackages() { - mockPackagesDir.listSync().forEach((FileSystemEntity entity) { - entity.deleteSync(recursive: true); - }); -} - -/// Run the command [runner] with the given [args] and return -/// what was printed. -Future> runCapturingPrint( - CommandRunner runner, List args) async { - final List prints = []; - final ZoneSpecification spec = ZoneSpecification( - print: (_, __, ___, String message) { - prints.add(message); - }, - ); - await Zone.current - .fork(specification: spec) - .run>(() => runner.run(args)); - - return prints; -} - -/// A mock [ProcessRunner] which records process calls. -class RecordingProcessRunner extends ProcessRunner { - io.Process processToReturn; - final List recordedCalls = []; - - /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. - String resultStdout; - - /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. - String resultStderr; - - @override - Future runAndStream( - String executable, - List args, { - Directory workingDir, - bool exitOnError = false, - }) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - return Future.value( - processToReturn == null ? 0 : await processToReturn.exitCode); - } - - /// Returns [io.ProcessResult] created from [processToReturn], [resultStdout], and [resultStderr]. - @override - Future run(String executable, List args, - {Directory workingDir, - bool exitOnError = false, - stdoutEncoding = io.systemEncoding, - stderrEncoding = io.systemEncoding}) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - io.ProcessResult result; - - if (processToReturn != null) { - result = io.ProcessResult( - processToReturn.pid, - await processToReturn.exitCode, - resultStdout ?? processToReturn.stdout, - resultStderr ?? processToReturn.stderr); - } - return Future.value(result); - } - - @override - Future runAndExitOnError( - String executable, - List args, { - Directory workingDir, - }) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - io.ProcessResult result; - if (processToReturn != null) { - result = io.ProcessResult( - processToReturn.pid, - await processToReturn.exitCode, - resultStdout ?? processToReturn.stdout, - resultStderr ?? processToReturn.stderr); - } - return Future.value(result); - } - - @override - Future start(String executable, List args, - {Directory workingDirectory}) async { - recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); - return Future.value(processToReturn); - } -} - -/// A recorded process call. -class ProcessCall { - const ProcessCall(this.executable, this.args, this.workingDir); - - /// The executable that was called. - final String executable; - - /// The arguments passed to [executable] in the call. - final List args; - - /// The working directory this process was called from. - final String workingDir; - - @override - bool operator ==(dynamic other) { - if (other is! ProcessCall) { - return false; - } - final ProcessCall otherCall = other; - return executable == otherCall.executable && - listsEqual(args, otherCall.args) && - workingDir == otherCall.workingDir; - } - - @override - int get hashCode => - executable?.hashCode ?? - 0 ^ args?.hashCode ?? - 0 ^ workingDir?.hashCode ?? - 0; - - @override - String toString() { - final List command = [executable]..addAll(args); - return '"${command.join(' ')}" in $workingDir'; - } -} diff --git a/packages/fluttter_plugin_tools/test/version_check_test.dart b/packages/fluttter_plugin_tools/test/version_check_test.dart deleted file mode 100644 index b9ace3811bff..000000000000 --- a/packages/fluttter_plugin_tools/test/version_check_test.dart +++ /dev/null @@ -1,319 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:git/git.dart'; -import 'package:mockito/mockito.dart'; -import "package:test/test.dart"; -import "package:flutter_plugin_tools/src/version_check_command.dart"; -import 'package:pub_semver/pub_semver.dart'; -import 'util.dart'; - -void testAllowedVersion( - String masterVersion, - String headVersion, { - bool allowed = true, - NextVersionType nextVersionType, -}) { - final Version master = Version.parse(masterVersion); - final Version head = Version.parse(headVersion); - final Map allowedVersions = - getAllowedNextVersions(master, head); - if (allowed) { - expect(allowedVersions, contains(head)); - if (nextVersionType != null) { - expect(allowedVersions[head], equals(nextVersionType)); - } - } else { - expect(allowedVersions, isNot(contains(head))); - } -} - -class MockGitDir extends Mock implements GitDir {} - -class MockProcessResult extends Mock implements ProcessResult {} - -void main() { - group('$VersionCheckCommand', () { - CommandRunner runner; - RecordingProcessRunner processRunner; - List> gitDirCommands; - String gitDiffResponse; - Map gitShowResponses; - - setUp(() { - gitDirCommands = >[]; - gitDiffResponse = ''; - gitShowResponses = {}; - final MockGitDir gitDir = MockGitDir(); - when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0]); - final MockProcessResult mockProcessResult = MockProcessResult(); - if (invocation.positionalArguments[0][0] == 'diff') { - when(mockProcessResult.stdout).thenReturn(gitDiffResponse); - } else if (invocation.positionalArguments[0][0] == 'show') { - final String response = - gitShowResponses[invocation.positionalArguments[0][1]]; - when(mockProcessResult.stdout).thenReturn(response); - } - return Future.value(mockProcessResult); - }); - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final VersionCheckCommand command = VersionCheckCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner, gitDir: gitDir); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); - }); - - tearDown(() { - cleanupPackages(); - }); - - test('allows valid version', () async { - createFakePlugin('plugin'); - gitDiffResponse = "packages/plugin/pubspec.yaml"; - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', - }; - final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - - expect( - output, - orderedEquals([ - 'No version check errors found!', - ]), - ); - expect(gitDirCommands.length, equals(3)); - expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect(gitDirCommands[1].join(' '), - equals('show master:packages/plugin/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin/pubspec.yaml')); - }); - - test('denies invalid version', () async { - createFakePlugin('plugin'); - gitDiffResponse = "packages/plugin/pubspec.yaml"; - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', - }; - final Future> result = runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - - await expectLater( - result, - throwsA(const TypeMatcher()), - ); - expect(gitDirCommands.length, equals(3)); - expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect(gitDirCommands[1].join(' '), - equals('show master:packages/plugin/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin/pubspec.yaml')); - }); - - test('gracefully handles missing pubspec.yaml', () async { - createFakePlugin('plugin'); - gitDiffResponse = "packages/plugin/pubspec.yaml"; - mockFileSystem.currentDirectory - .childDirectory('packages') - .childDirectory('plugin') - .childFile('pubspec.yaml') - .deleteSync(); - final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - - expect( - output, - orderedEquals([ - 'No version check errors found!', - ]), - ); - expect(gitDirCommands.length, equals(1)); - expect(gitDirCommands.first.join(' '), - equals('diff --name-only master HEAD')); - }); - - test('allows minor changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface'); - gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; - gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.0.0', - 'HEAD:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.1.0', - }; - final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - expect( - output, - orderedEquals([ - 'No version check errors found!', - ]), - ); - expect(gitDirCommands.length, equals(3)); - expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect( - gitDirCommands[1].join(' '), - equals( - 'show master:packages/plugin_platform_interface/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); - }); - - test('disallows breaking changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface'); - gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; - gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.0.0', - 'HEAD:packages/plugin_platform_interface/pubspec.yaml': - 'version: 2.0.0', - }; - final Future> output = runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - await expectLater( - output, - throwsA(const TypeMatcher()), - ); - expect(gitDirCommands.length, equals(3)); - expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect( - gitDirCommands[1].join(' '), - equals( - 'show master:packages/plugin_platform_interface/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); - }); - }); - - group("Pre 1.0", () { - test("nextVersion allows patch version", () { - testAllowedVersion("0.12.0", "0.12.0+1", - nextVersionType: NextVersionType.PATCH); - testAllowedVersion("0.12.0+4", "0.12.0+5", - nextVersionType: NextVersionType.PATCH); - }); - - test("nextVersion does not allow jumping patch", () { - testAllowedVersion("0.12.0", "0.12.0+2", allowed: false); - testAllowedVersion("0.12.0+2", "0.12.0+4", allowed: false); - }); - - test("nextVersion does not allow going back", () { - testAllowedVersion("0.12.0", "0.11.0", allowed: false); - testAllowedVersion("0.12.0+2", "0.12.0+1", allowed: false); - testAllowedVersion("0.12.0+1", "0.12.0", allowed: false); - }); - - test("nextVersion allows minor version", () { - testAllowedVersion("0.12.0", "0.12.1", - nextVersionType: NextVersionType.MINOR); - testAllowedVersion("0.12.0+4", "0.12.1", - nextVersionType: NextVersionType.MINOR); - }); - - test("nextVersion does not allow jumping minor", () { - testAllowedVersion("0.12.0", "0.12.2", allowed: false); - testAllowedVersion("0.12.0+2", "0.12.3", allowed: false); - }); - }); - - group("Releasing 1.0", () { - test("nextVersion allows releasing 1.0", () { - testAllowedVersion("0.12.0", "1.0.0", - nextVersionType: NextVersionType.BREAKING_MAJOR); - testAllowedVersion("0.12.0+4", "1.0.0", - nextVersionType: NextVersionType.BREAKING_MAJOR); - }); - - test("nextVersion does not allow jumping major", () { - testAllowedVersion("0.12.0", "2.0.0", allowed: false); - testAllowedVersion("0.12.0+4", "2.0.0", allowed: false); - }); - - test("nextVersion does not allow un-releasing", () { - testAllowedVersion("1.0.0", "0.12.0+4", allowed: false); - testAllowedVersion("1.0.0", "0.12.0", allowed: false); - }); - }); - - group("Post 1.0", () { - test("nextVersion allows patch jumps", () { - testAllowedVersion("1.0.1", "1.0.2", - nextVersionType: NextVersionType.PATCH); - testAllowedVersion("1.0.0", "1.0.1", - nextVersionType: NextVersionType.PATCH); - }); - - test("nextVersion does not allow build jumps", () { - testAllowedVersion("1.0.1", "1.0.1+1", allowed: false); - testAllowedVersion("1.0.0+5", "1.0.0+6", allowed: false); - }); - - test("nextVersion does not allow skipping patches", () { - testAllowedVersion("1.0.1", "1.0.3", allowed: false); - testAllowedVersion("1.0.0", "1.0.6", allowed: false); - }); - - test("nextVersion allows minor version jumps", () { - testAllowedVersion("1.0.1", "1.1.0", - nextVersionType: NextVersionType.MINOR); - testAllowedVersion("1.0.0", "1.1.0", - nextVersionType: NextVersionType.MINOR); - }); - - test("nextVersion does not allow skipping minor versions", () { - testAllowedVersion("1.0.1", "1.2.0", allowed: false); - testAllowedVersion("1.1.0", "1.3.0", allowed: false); - }); - - test("nextVersion allows breaking changes", () { - testAllowedVersion("1.0.1", "2.0.0", - nextVersionType: NextVersionType.BREAKING_MAJOR); - testAllowedVersion("1.0.0", "2.0.0", - nextVersionType: NextVersionType.BREAKING_MAJOR); - }); - - test("nextVersion allows null safety pre prelease", () { - testAllowedVersion("1.0.1", "2.0.0-nullsafety", - nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0", "2.0.0-nullsafety", - nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0-nullsafety", "1.0.0-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0-nullsafety.1", "1.0.0-nullsafety.2", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0", "0.2.0-nullsafety", - nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0-nullsafety", "0.1.0-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0-nullsafety.1", "0.1.0-nullsafety.2", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0", "1.1.0-nullsafety", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.1.0-nullsafety", "1.1.0-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0", "0.1.1-nullsafety", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.1-nullsafety", "0.1.1-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - }); - - test("nextVersion does not allow skipping major versions", () { - testAllowedVersion("1.0.1", "3.0.0", allowed: false); - testAllowedVersion("1.1.0", "2.3.0", allowed: false); - }); - }); -} diff --git a/packages/fluttter_plugin_tools/test/xctest_command_test.dart b/packages/fluttter_plugin_tools/test/xctest_command_test.dart deleted file mode 100644 index 007c2e12188c..000000000000 --- a/packages/fluttter_plugin_tools/test/xctest_command_test.dart +++ /dev/null @@ -1,358 +0,0 @@ -// 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 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/xctest_command.dart'; -import 'package:test/test.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -final _kDeviceListMap = { - "runtimes": [ - { - "bundlePath": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime", - "buildversion": "17A577", - "runtimeRoot": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot", - "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-0", - "version": "13.0", - "isAvailable": true, - "name": "iOS 13.0" - }, - { - "bundlePath": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime", - "buildversion": "17L255", - "runtimeRoot": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot", - "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-4", - "version": "13.4", - "isAvailable": true, - "name": "iOS 13.4" - }, - { - "bundlePath": - "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime", - "buildversion": "17T531", - "runtimeRoot": - "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot", - "identifier": "com.apple.CoreSimulator.SimRuntime.watchOS-6-2", - "version": "6.2.1", - "isAvailable": true, - "name": "watchOS 6.2" - } - ], - "devices": { - "com.apple.CoreSimulator.SimRuntime.iOS-13-4": [ - { - "dataPath": - "/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data", - "logPath": - "/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774", - "udid": "2706BBEB-1E01-403E-A8E9-70E8E5A24774", - "isAvailable": true, - "deviceTypeIdentifier": - "com.apple.CoreSimulator.SimDeviceType.iPhone-8", - "state": "Shutdown", - "name": "iPhone 8" - }, - { - "dataPath": - "/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data", - "logPath": - "/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A", - "udid": "1E76A0FD-38AC-4537-A989-EA639D7D012A", - "isAvailable": true, - "deviceTypeIdentifier": - "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus", - "state": "Shutdown", - "name": "iPhone 8 Plus" - } - ] - } -}; - -void main() { - const String _kDestination = '--ios-destination'; - const String _kTarget = '--target'; - const String _kSkip = '--skip'; - - group('test xctest_command', () { - CommandRunner runner; - RecordingProcessRunner processRunner; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final XCTestCommand command = XCTestCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner('xctest_command', 'Test for xctest_command'); - runner.addCommand(command); - cleanupPackages(); - }); - - test('Not specifying --target throws', () async { - await expectLater( - () => runner.run(['xctest', _kDestination, 'a_destination']), - throwsA(const TypeMatcher())); - }); - - test('skip if ios is not supported', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - final List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination' - ]); - expect(output, contains('iOS is not supported by this plugin.')); - expect(processRunner.recordedCalls, orderedEquals([])); - - cleanupPackages(); - }); - - test('running with correct scheme and destination, did not find scheme', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - processRunner.resultStdout = '{"project":{"targets":["bar_scheme"]}}'; - - await expectLater(() async { - final List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination' - ]); - expect(output, - contains('foo_scheme not configured for plugin, test failed.')); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('xcrun', ['simctl', 'list', '--json'], null), - ProcessCall( - 'xcodebuild', - [ - '-project', - 'ios/Runner.xcodeproj', - '-list', - '-json' - ], - pluginExampleDirectory.path), - ])); - }, throwsA(const TypeMatcher())); - cleanupPackages(); - }); - - test('running with correct scheme and destination, found scheme', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; - List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination' - ]); - - expect(output, contains('Successfully ran xctest for plugin')); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcodebuild', - ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], - pluginExampleDirectory.path), - ProcessCall( - 'xcodebuild', - [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'foo_scheme', - '-destination', - 'foo_destination', - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ], - pluginExampleDirectory.path), - ])); - - cleanupPackages(); - }); - - test('running with correct scheme and destination, skip 1 plugin', - () async { - createFakePlugin('plugin1', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - createFakePlugin('plugin2', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory1 = - mockPackagesDir.childDirectory('plugin1').childDirectory('example'); - createFakePubspec(pluginExampleDirectory1, isFlutter: true); - final Directory pluginExampleDirectory2 = - mockPackagesDir.childDirectory('plugin2').childDirectory('example'); - createFakePubspec(pluginExampleDirectory2, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; - List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination', - _kSkip, - 'plugin1' - ]); - - expect(output, contains('plugin1 was skipped with the --skip flag.')); - expect(output, contains('Successfully ran xctest for plugin2')); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcodebuild', - ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], - pluginExampleDirectory2.path), - ProcessCall( - 'xcodebuild', - [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'foo_scheme', - '-destination', - 'foo_destination', - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ], - pluginExampleDirectory2.path), - ])); - - cleanupPackages(); - }); - - test('Not specifying --ios-destination assigns an available simulator', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - final Map schemeCommandResult = { - "project": { - "targets": ["bar_scheme", "foo_scheme"] - } - }; - // For simplicity of the test, we combine all the mock results into a single mock result, each internal command - // will get this result and they should still be able to parse them correctly. - processRunner.resultStdout = - jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); - await runner.run([ - 'xctest', - _kTarget, - 'foo_scheme', - ]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('xcrun', ['simctl', 'list', '--json'], null), - ProcessCall( - 'xcodebuild', - ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], - pluginExampleDirectory.path), - ProcessCall( - 'xcodebuild', - [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'foo_scheme', - '-destination', - 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ], - pluginExampleDirectory.path), - ])); - - cleanupPackages(); - }); - }); -} diff --git a/packages/plugin_tools/CHANGELOG.md b/packages/plugin_tools/CHANGELOG.md deleted file mode 100644 index 25dce424eee4..000000000000 --- a/packages/plugin_tools/CHANGELOG.md +++ /dev/null @@ -1,288 +0,0 @@ -## v.0.0.45+1 - -- Don't call `flutter format` if there are no Dart files to format. - -## v.0.0.45 - -- Add exclude flag to exclude any plugin from further processing. - -## v.0.0.44+7 - -- `all-plugins-app` doesn't override the AGP version. - -## v.0.0.44+6 - -- Fix code formatting. - -## v.0.0.44+5 - -- Remove `-v` flag on drive-examples. - -## v.0.0.44+4 - -- Fix bug where directory isn't passed - -## v.0.0.44+3 - -- More verbose logging - -## v.0.0.44+2 - -- Remove pre-alpha Windows workaround to create examples on the fly. - -## v.0.0.44+1 - -- Print packages that passed tests in `xctest` command. -- Remove printing the whole list of simulators. - -## v.0.0.44 - -- Add 'xctest' command to run xctests. - -## v.0.0.43 - -- Allow minor `*-nullsafety` pre release packages. - -## v.0.0.42+1 - -- Fix test command when `--enable-experiment` is called. - -## v.0.0.42 - -- Allow `*-nullsafety` pre release packages. - -## v.0.0.41 - -- Support `--enable-experiment` flag in subcommands `test`, `build-examples`, `drive-examples`, -and `firebase-test-lab`. - -## v.0.0.40 - -- Support `integration_test/` directory for `drive-examples` command - -## v.0.0.39 - -- Support `integration_test/` directory for `package:integration_test` - -## v.0.0.38 - -- Add C++ and ObjC++ to clang-format. - -## v.0.0.37+2 - -- Make `http` and `http_multi_server` dependency version constraint more flexible. - -## v.0.0.37+1 - -- All_plugin test puts the plugin dependencies into dependency_overrides. - -## v.0.0.37 - -- Only builds mobile example apps when necessary. - -## v.0.0.36+3 - -- Add support for Linux plugins. - -## v.0.0.36+2 - -- Default to showing podspec lint warnings - -## v.0.0.36+1 - -- Serialize linting podspecs. - -## v.0.0.36 - -- Remove retry on Firebase Test Lab's call to gcloud set. -- Remove quiet flag from Firebase Test Lab's gcloud set command. -- Allow Firebase Test Lab command to continue past gcloud set network failures. - This is a mitigation for the network service sometimes not responding, - but it isn't actually necessary to have a network connection for this command. - -## v.0.0.35+1 - -- Minor cleanup to the analyze test. - -## v.0.0.35 - -- Firebase Test Lab command generates a configurable unique path suffix for results. - -## v.0.0.34 - -- Firebase Test Lab command now only tries to configure the project once -- Firebase Test Lab command now retries project configuration up to five times. - -## v.0.0.33+1 - -- Fixes formatting issues that got past our CI due to - https://github.com/flutter/flutter/issues/51585. -- Changes the default package name for testing method `createFakePubspec` back - its previous behavior. - -## v.0.0.33 - -- Version check command now fails on breaking changes to platform interfaces. -- Updated version check test to be more flexible. - -## v.0.0.32+7 - -- Ensure that Firebase Test Lab tests have a unique storage bucket for each test run. - -## v.0.0.32+6 - -- Ensure that Firebase Test Lab tests have a unique storage bucket for each package. - -## v.0.0.32+5 - -- Remove --fail-fast and --silent from lint podspec command. - -## v.0.0.32+4 - -- Update `publish-plugin` to use `flutter pub publish` instead of just `pub - publish`. Enforces a `pub publish` command that matches the Dart SDK in the - user's Flutter install. - -## v.0.0.32+3 - -- Update Firebase Testlab deprecated test device. (Pixel 3 API 28 -> Pixel 4 API 29). - -## v.0.0.32+2 - -- Runs pub get before building macos to avoid failures. - -## v.0.0.32+1 - -- Default macOS example builds to false. Previously they were running whenever - CI was itself running on macOS. - -## v.0.0.32 - -- `analyze` now asserts that the global `analysis_options.yaml` is the only one - by default. Individual directories can be excluded from this check with the - new `--custom-analysis` flag. - -## v.0.0.31+1 - -- Add --skip and --no-analyze flags to podspec command. - -## v.0.0.31 - -- Add support for macos on `DriveExamplesCommand` and `BuildExamplesCommand`. - -## v.0.0.30 - -- Adopt pedantic analysis options, fix firebase_test_lab_test. - -## v.0.0.29 - -- Add a command to run pod lib lint on podspec files. - -## v.0.0.28 - -- Increase Firebase test lab timeouts to 5 minutes. - -## v.0.0.27 - -- Run tests with `--platform=chrome` for web plugins. - -## v.0.0.26 - -- Add a command for publishing plugins to pub. - -## v.0.0.25 - -- Update `DriveExamplesCommand` to use `ProcessRunner`. -- Make `DriveExamplesCommand` rely on `ProcessRunner` to determine if the test fails or not. -- Add simple tests for `DriveExamplesCommand`. - -## v.0.0.24 - -- Gracefully handle pubspec.yaml files for new plugins. -- Additional unit testing. - -## v.0.0.23 - -- Add a test case for transitive dependency solving in the - `create_all_plugins_app` command. - -## v.0.0.22 - -- Updated firebase-test-lab command with updated conventions for test locations. -- Updated firebase-test-lab to add an optional "device" argument. -- Updated version-check command to always compare refs instead of using the working copy. -- Added unit tests for the firebase-test-lab and version-check commands. -- Add ProcessRunner to mock running processes for testing. - -## v.0.0.21 - -- Support the `--plugins` argument for federated plugins. - -## v.0.0.20 - -- Support for finding federated plugins, where one directory contains - multiple packages for different platform implementations. - -## v.0.0.19+3 - -- Use `package:file` for file I/O. - -## v.0.0.19+2 - -- Use java as language when calling `flutter create`. - -## v.0.0.19+1 - -- Rename command for `CreateAllPluginsAppCommand`. - -## v.0.0.19 - -- Use flutter create to build app testing plugin compilation. - -## v.0.0.18+2 - -- Fix `.travis.yml` file name in `README.md`. - -## v0.0.18+1 - -- Skip version check if it contains `publish_to: none`. - -## v0.0.18 - -- Add option to exclude packages from generated pubspec command. - -## v0.0.17+4 - -- Avoid trying to version-check pubspecs that are missing a version. - -## v0.0.17+3 - -- version-check accounts for [pre-1.0 patch versions](https://github.com/flutter/flutter/issues/35412). - -## v0.0.17+2 - -- Fix exception handling for version checker - -## v0.0.17+1 - -- Fix bug where we used a flag instead of an option - -## v0.0.17 - -- Add a command for checking the version number - -## v0.0.16 - -- Add a command for generating `pubspec.yaml` for All Plugins app. - -## v0.0.15 - -- Add a command for running driver tests of plugin examples. - -## v0.0.14 - -- Check for dependencies->flutter instead of top level flutter node. - -## v0.0.13 - -- Differentiate between Flutter and non-Flutter (but potentially Flutter consumed) Dart packages. diff --git a/packages/plugin_tools/LICENSE b/packages/plugin_tools/LICENSE deleted file mode 100644 index 5f015bc7b321..000000000000 --- a/packages/plugin_tools/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2017, Flutter -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/plugin_tools/README.md b/packages/plugin_tools/README.md deleted file mode 100644 index efdc668e81a0..000000000000 --- a/packages/plugin_tools/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Flutter Plugin Tools - -[![Build Status](https://travis-ci.org/flutter/plugin_tools.svg?branch=master)](https://travis-ci.org/flutter/plugin_tools) -[![pub package](https://img.shields.io/pub/v/flutter_plugin_tools.svg)](https://pub.dartlang.org/packages/flutter_plugin_tools) - - -Flutter Plugin Tools implements a CLI with various productivity tools for hosting multiple Flutter plugins in one github -repository. It is mainly used by the [flutter/plugins](https://github.com/flutter/plugins) and -[flutter/flutterfire](https://github.com/flutter/flutterfire) repositories. It was mainly written to facilitate -testing on Travis for these repositories (see [.travis.yml](https://github.com/flutter/plugins/blob/master/.travis.yml)). - -As an example, Flutter Plugin Tools allows you to: - -* Build all plugin example apps with one command -* Run the tests of all plugins with one command -* Format all Dart, Java, Objective-C, and C++ code in the repository -* Define shards of the above tasks - -## Installation - -In order to use the tools you need to enable them once by running the following command: - -```shell -$ pub global activate flutter_plugin_tools -``` - -## Requirements - -To use all features of `flutter_plugin_tools` you'll need the following commands in your `PATH`: -* `flutter` -* `git` -* `pub` (recommended: version from `/bin/cache/dart-sdk/bin`) -* `clang-format` version 5 (alternatively, you can provide the path via `--clang-format=`) -* [`pod`](https://guides.cocoapods.org/using/getting-started.html#installation) (macOS only) - -## Usage - -```shell -$ pub global run flutter_plugin_tools -$ pub global run flutter_plugin_tools --shardIndex 0 --shardCount 3 -``` - -Run commands from the `flutter/plugins` directory. Replace `` with `help` to print a list of available commands. -The sharded example above divides the plugins into three shards -and executes the tool on the first shard (index 0). diff --git a/packages/plugin_tools/analysis_options.yaml b/packages/plugin_tools/analysis_options.yaml deleted file mode 100644 index 84a5e26f95de..000000000000 --- a/packages/plugin_tools/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: package:pedantic/analysis_options.1.8.0.yaml diff --git a/packages/plugin_tools/bin/flutter_plugin_tools.dart b/packages/plugin_tools/bin/flutter_plugin_tools.dart deleted file mode 100644 index 43edcbfe2c68..000000000000 --- a/packages/plugin_tools/bin/flutter_plugin_tools.dart +++ /dev/null @@ -1,5 +0,0 @@ -// 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. - -export 'package:flutter_plugin_tools/src/main.dart'; diff --git a/packages/plugin_tools/lib/src/analyze_command.dart b/packages/plugin_tools/lib/src/analyze_command.dart deleted file mode 100644 index 8cd57fa0b338..000000000000 --- a/packages/plugin_tools/lib/src/analyze_command.dart +++ /dev/null @@ -1,94 +0,0 @@ -// 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:file/file.dart'; -import 'package:path/path.dart' as p; - -import 'common.dart'; - -class AnalyzeCommand extends PluginCommand { - AnalyzeCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addMultiOption(_customAnalysisFlag, - help: - 'Directories (comma seperated) that are allowed to have their own analysis options.', - defaultsTo: []); - } - - static const String _customAnalysisFlag = 'custom-analysis'; - - @override - final String name = 'analyze'; - - @override - final String description = 'Analyzes all packages using package:tuneup.\n\n' - 'This command requires "pub" and "flutter" to be in your path.'; - - @override - Future run() async { - checkSharding(); - - print('Verifying analysis settings...'); - final List files = packagesDir.listSync(recursive: true); - for (final FileSystemEntity file in files) { - if (file.basename != 'analysis_options.yaml' && - file.basename != '.analysis_options') { - continue; - } - - final bool whitelisted = argResults[_customAnalysisFlag].any( - (String directory) => - p.isWithin(p.join(packagesDir.path, directory), file.path)); - if (whitelisted) { - continue; - } - - print('Found an extra analysis_options.yaml in ${file.absolute.path}.'); - print( - 'If this was deliberate, pass the package to the analyze command with the --$_customAnalysisFlag flag and try again.'); - throw ToolExit(1); - } - - print('Activating tuneup package...'); - await processRunner.runAndStream( - 'pub', ['global', 'activate', 'tuneup'], - workingDir: packagesDir, exitOnError: true); - - await for (Directory package in getPackages()) { - if (isFlutterPackage(package, fileSystem)) { - await processRunner.runAndStream('flutter', ['packages', 'get'], - workingDir: package, exitOnError: true); - } else { - await processRunner.runAndStream('pub', ['get'], - workingDir: package, exitOnError: true); - } - } - - final List failingPackages = []; - await for (Directory package in getPlugins()) { - final int exitCode = await processRunner.runAndStream( - 'pub', ['global', 'run', 'tuneup', 'check'], - workingDir: package); - if (exitCode != 0) { - failingPackages.add(p.basename(package.path)); - } - } - - print('\n\n'); - if (failingPackages.isNotEmpty) { - print('The following packages have analyzer errors (see above):'); - failingPackages.forEach((String package) { - print(' * $package'); - }); - throw ToolExit(1); - } - - print('No analyzer errors found!'); - } -} diff --git a/packages/plugin_tools/lib/src/build_examples_command.dart b/packages/plugin_tools/lib/src/build_examples_command.dart deleted file mode 100644 index 1493b3cce05d..000000000000 --- a/packages/plugin_tools/lib/src/build_examples_command.dart +++ /dev/null @@ -1,191 +0,0 @@ -// 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 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; - -import 'common.dart'; - -class BuildExamplesCommand extends PluginCommand { - BuildExamplesCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addFlag(kLinux, defaultsTo: false); - argParser.addFlag(kMacos, defaultsTo: false); - argParser.addFlag(kWindows, defaultsTo: false); - argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS); - argParser.addFlag(kApk); - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: 'Enables the given Dart SDK experiments.', - ); - } - - @override - final String name = 'build-examples'; - - @override - final String description = - 'Builds all example apps (IPA for iOS and APK for Android).\n\n' - 'This command requires "flutter" to be in your path.'; - - @override - Future run() async { - if (!argResults[kIpa] && - !argResults[kApk] && - !argResults[kLinux] && - !argResults[kMacos] && - !argResults[kWindows]) { - print( - 'None of --linux, --macos, --windows, --apk nor --ipa were specified, ' - 'so not building anything.'); - return; - } - final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - - final String enableExperiment = argResults[kEnableExperiment]; - - checkSharding(); - final List failingPackages = []; - await for (Directory plugin in getPlugins()) { - for (Directory example in getExamplesForPlugin(plugin)) { - final String packageName = - p.relative(example.path, from: packagesDir.path); - - if (argResults[kLinux]) { - print('\nBUILDING Linux for $packageName'); - if (isLinuxPlugin(plugin, fileSystem)) { - int buildExitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kLinux, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (buildExitCode != 0) { - failingPackages.add('$packageName (linux)'); - } - } else { - print('Linux is not supported by this plugin'); - } - } - - if (argResults[kMacos]) { - print('\nBUILDING macOS for $packageName'); - if (isMacOsPlugin(plugin, fileSystem)) { - // TODO(https://github.com/flutter/flutter/issues/46236): - // Builing macos without running flutter pub get first results - // in an error. - int exitCode = await processRunner.runAndStream( - flutterCommand, ['pub', 'get'], - workingDir: example); - if (exitCode != 0) { - failingPackages.add('$packageName (macos)'); - } else { - exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kMacos, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (exitCode != 0) { - failingPackages.add('$packageName (macos)'); - } - } - } else { - print('macOS is not supported by this plugin'); - } - } - - if (argResults[kWindows]) { - print('\nBUILDING Windows for $packageName'); - if (isWindowsPlugin(plugin, fileSystem)) { - int buildExitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kWindows, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (buildExitCode != 0) { - failingPackages.add('$packageName (windows)'); - } - } else { - print('Windows is not supported by this plugin'); - } - } - - if (argResults[kIpa]) { - print('\nBUILDING IPA for $packageName'); - if (isIosPlugin(plugin, fileSystem)) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - 'ios', - '--no-codesign', - '--verbose', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example, - exitOnError: true); - if (exitCode != 0) { - failingPackages.add('$packageName (ipa)'); - } - } else { - print('iOS is not supported by this plugin'); - } - } - print('end ios build'); - - if (argResults[kApk]) { - print('\nBUILDING APK for $packageName'); - if (isAndroidPlugin(plugin, fileSystem)) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - 'apk', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (exitCode != 0) { - failingPackages.add('$packageName (apk)'); - } - } else { - print('Android is not supported by this plugin'); - } - } - } - } - print('\n\n'); - - if (failingPackages.isNotEmpty) { - print('The following build are failing (see above for details):'); - for (String package in failingPackages) { - print(' * $package'); - } - throw ToolExit(1); - } - - print('All builds successful!'); - } -} diff --git a/packages/plugin_tools/lib/src/common.dart b/packages/plugin_tools/lib/src/common.dart deleted file mode 100644 index 78b91ee8a75b..000000000000 --- a/packages/plugin_tools/lib/src/common.dart +++ /dev/null @@ -1,466 +0,0 @@ -// 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 'dart:io' as io; -import 'dart:math'; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:yaml/yaml.dart'; - -typedef void Print(Object object); - -/// Key for windows platform. -const String kWindows = 'windows'; - -/// Key for macos platform. -const String kMacos = 'macos'; - -/// Key for linux platform. -const String kLinux = 'linux'; - -/// Key for IPA (iOS) platform. -const String kIos = 'ios'; - -/// Key for APK (Android) platform. -const String kAndroid = 'android'; - -/// Key for Web platform. -const String kWeb = 'web'; - -/// Key for IPA. -const String kIpa = 'ipa'; - -/// Key for APK. -const String kApk = 'apk'; - -/// Key for enable experiment. -const String kEnableExperiment = 'enable-experiment'; - -/// Returns whether the given directory contains a Flutter package. -bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) { - if (entity == null || entity is! Directory) { - return false; - } - - try { - final File pubspecFile = - fileSystem.file(p.join(entity.path, 'pubspec.yaml')); - final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); - final YamlMap dependencies = pubspecYaml['dependencies']; - if (dependencies == null) { - return false; - } - return dependencies.containsKey('flutter'); - } on FileSystemException { - return false; - } on YamlException { - return false; - } -} - -/// Returns whether the given directory contains a Flutter [platform] plugin. -/// -/// It checks this by looking for the following pattern in the pubspec: -/// -/// flutter: -/// plugin: -/// platforms: -/// [platform]: -bool pluginSupportsPlatform( - String platform, FileSystemEntity entity, FileSystem fileSystem) { - assert(platform == kIos || - platform == kAndroid || - platform == kWeb || - platform == kMacos || - platform == kWindows || - platform == kLinux); - if (entity == null || entity is! Directory) { - return false; - } - - try { - final File pubspecFile = - fileSystem.file(p.join(entity.path, 'pubspec.yaml')); - final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); - final YamlMap flutterSection = pubspecYaml['flutter']; - if (flutterSection == null) { - return false; - } - final YamlMap pluginSection = flutterSection['plugin']; - if (pluginSection == null) { - return false; - } - final YamlMap platforms = pluginSection['platforms']; - if (platforms == null) { - // Legacy plugin specs are assumed to support iOS and Android. - if (!pluginSection.containsKey('platforms')) { - return platform == kIos || platform == kAndroid; - } - return false; - } - return platforms.containsKey(platform); - } on FileSystemException { - return false; - } on YamlException { - return false; - } -} - -/// Returns whether the given directory contains a Flutter Android plugin. -bool isAndroidPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kAndroid, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter iOS plugin. -bool isIosPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kIos, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter web plugin. -bool isWebPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kWeb, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter Windows plugin. -bool isWindowsPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kWindows, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter macOS plugin. -bool isMacOsPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kMacos, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter linux plugin. -bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kLinux, entity, fileSystem); -} - -/// Error thrown when a command needs to exit with a non-zero exit code. -class ToolExit extends Error { - ToolExit(this.exitCode); - - final int exitCode; -} - -abstract class PluginCommand extends Command { - PluginCommand( - this.packagesDir, - this.fileSystem, { - this.processRunner = const ProcessRunner(), - }) { - argParser.addMultiOption( - _pluginsArg, - splitCommas: true, - help: - 'Specifies which plugins the command should run on (before sharding).', - valueHelp: 'plugin1,plugin2,...', - ); - argParser.addOption( - _shardIndexArg, - help: 'Specifies the zero-based index of the shard to ' - 'which the command applies.', - valueHelp: 'i', - defaultsTo: '0', - ); - argParser.addOption( - _shardCountArg, - help: 'Specifies the number of shards into which plugins are divided.', - valueHelp: 'n', - defaultsTo: '1', - ); - argParser.addMultiOption( - _excludeArg, - abbr: 'e', - help: 'Exclude packages from this command.', - defaultsTo: [], - ); - } - - static const String _pluginsArg = 'plugins'; - static const String _shardIndexArg = 'shardIndex'; - static const String _shardCountArg = 'shardCount'; - static const String _excludeArg = 'exclude'; - - /// The directory containing the plugin packages. - final Directory packagesDir; - - /// The file system. - /// - /// This can be overridden for testing. - final FileSystem fileSystem; - - /// The process runner. - /// - /// This can be overridden for testing. - final ProcessRunner processRunner; - - int _shardIndex; - int _shardCount; - - int get shardIndex { - if (_shardIndex == null) { - checkSharding(); - } - return _shardIndex; - } - - int get shardCount { - if (_shardCount == null) { - checkSharding(); - } - return _shardCount; - } - - void checkSharding() { - final int shardIndex = int.tryParse(argResults[_shardIndexArg]); - final int shardCount = int.tryParse(argResults[_shardCountArg]); - if (shardIndex == null) { - usageException('$_shardIndexArg must be an integer'); - } - if (shardCount == null) { - usageException('$_shardCountArg must be an integer'); - } - if (shardCount < 1) { - usageException('$_shardCountArg must be positive'); - } - if (shardIndex < 0 || shardCount <= shardIndex) { - usageException( - '$_shardIndexArg must be in the half-open range [0..$shardCount['); - } - _shardIndex = shardIndex; - _shardCount = shardCount; - } - - /// Returns the root Dart package folders of the plugins involved in this - /// command execution. - Stream getPlugins() async* { - // To avoid assuming consistency of `Directory.list` across command - // invocations, we collect and sort the plugin folders before sharding. - // This is considered an implementation detail which is why the API still - // uses streams. - final List allPlugins = await _getAllPlugins().toList(); - allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path)); - // Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2. - // Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3. - // Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0. - final int shardSize = allPlugins.length ~/ shardCount + - (allPlugins.length % shardCount == 0 ? 0 : 1); - final int start = min(shardIndex * shardSize, allPlugins.length); - final int end = min(start + shardSize, allPlugins.length); - - for (Directory plugin in allPlugins.sublist(start, end)) { - yield plugin; - } - } - - /// Returns the root Dart package folders of the plugins involved in this - /// command execution, assuming there is only one shard. - /// - /// Plugin packages can exist in one of two places relative to the packages - /// directory. - /// - /// 1. As a Dart package in a directory which is a direct child of the - /// packages directory. This is a plugin where all of the implementations - /// exist in a single Dart package. - /// 2. Several plugin packages may live in a directory which is a direct - /// child of the packages directory. This directory groups several Dart - /// packages which implement a single plugin. This directory contains a - /// "client library" package, which declares the API for the plugin, as - /// well as one or more platform-specific implementations. - Stream _getAllPlugins() async* { - final Set plugins = Set.from(argResults[_pluginsArg]); - final Set excludedPlugins = - Set.from(argResults[_excludeArg]); - - await for (FileSystemEntity entity - in packagesDir.list(followLinks: false)) { - // A top-level Dart package is a plugin package. - if (_isDartPackage(entity)) { - if (!excludedPlugins.contains(entity.basename) && - (plugins.isEmpty || plugins.contains(p.basename(entity.path)))) { - yield entity; - } - } else if (entity is Directory) { - // Look for Dart packages under this top-level directory. - await for (FileSystemEntity subdir in entity.list(followLinks: false)) { - if (_isDartPackage(subdir)) { - // If --plugin=my_plugin is passed, then match all federated - // plugins under 'my_plugin'. Also match if the exact plugin is - // passed. - final String relativePath = - p.relative(subdir.path, from: packagesDir.path); - final String basenamePath = p.basename(entity.path); - if (!excludedPlugins.contains(basenamePath) && - !excludedPlugins.contains(relativePath) && - (plugins.isEmpty || - plugins.contains(relativePath) || - plugins.contains(basenamePath))) { - yield subdir; - } - } - } - } - } - } - - /// Returns the example Dart package folders of the plugins involved in this - /// command execution. - Stream getExamples() => - getPlugins().expand(getExamplesForPlugin); - - /// Returns all Dart package folders (typically, plugin + example) of the - /// plugins involved in this command execution. - Stream getPackages() async* { - await for (Directory plugin in getPlugins()) { - yield plugin; - yield* plugin - .list(recursive: true, followLinks: false) - .where(_isDartPackage) - .cast(); - } - } - - /// Returns the files contained, recursively, within the plugins - /// involved in this command execution. - Stream getFiles() { - return getPlugins().asyncExpand((Directory folder) => folder - .list(recursive: true, followLinks: false) - .where((FileSystemEntity entity) => entity is File) - .cast()); - } - - /// Returns whether the specified entity is a directory containing a - /// `pubspec.yaml` file. - bool _isDartPackage(FileSystemEntity entity) { - return entity is Directory && - fileSystem.file(p.join(entity.path, 'pubspec.yaml')).existsSync(); - } - - /// Returns the example Dart packages contained in the specified plugin, or - /// an empty List, if the plugin has no examples. - Iterable getExamplesForPlugin(Directory plugin) { - final Directory exampleFolder = - fileSystem.directory(p.join(plugin.path, 'example')); - if (!exampleFolder.existsSync()) { - return []; - } - if (isFlutterPackage(exampleFolder, fileSystem)) { - return [exampleFolder]; - } - // Only look at the subdirectories of the example directory if the example - // directory itself is not a Dart package, and only look one level below the - // example directory for other dart packages. - return exampleFolder - .listSync() - .where( - (FileSystemEntity entity) => isFlutterPackage(entity, fileSystem)) - .cast(); - } -} - -/// A class used to run processes. -/// -/// We use this instead of directly running the process so it can be overridden -/// in tests. -class ProcessRunner { - const ProcessRunner(); - - /// Run the [executable] with [args] and stream output to stderr and stdout. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// If [exitOnError] is set to `true`, then this will throw an error if - /// the [executable] terminates with a non-zero exit code. - /// - /// Returns the exit code of the [executable]. - Future runAndStream( - String executable, - List args, { - Directory workingDir, - bool exitOnError = false, - }) async { - print( - 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); - final io.Process process = await io.Process.start(executable, args, - workingDirectory: workingDir?.path); - await io.stdout.addStream(process.stdout); - await io.stderr.addStream(process.stderr); - if (exitOnError && await process.exitCode != 0) { - final String error = - _getErrorString(executable, args, workingDir: workingDir); - print('$error See above for details.'); - throw ToolExit(await process.exitCode); - } - return process.exitCode; - } - - /// Run the [executable] with [args]. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// If [exitOnError] is set to `true`, then this will throw an error if - /// the [executable] terminates with a non-zero exit code. - /// - /// Returns the [io.ProcessResult] of the [executable]. - Future run(String executable, List args, - {Directory workingDir, - bool exitOnError = false, - stdoutEncoding = io.systemEncoding, - stderrEncoding = io.systemEncoding}) async { - return io.Process.run(executable, args, - workingDirectory: workingDir?.path, - stdoutEncoding: stdoutEncoding, - stderrEncoding: stderrEncoding); - } - - /// Starts the [executable] with [args]. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// Returns the started [io.Process]. - Future start(String executable, List args, - {Directory workingDirectory}) async { - final io.Process process = await io.Process.start(executable, args, - workingDirectory: workingDirectory?.path); - return process; - } - - /// Run the [executable] with [args], throwing an error on non-zero exit code. - /// - /// Unlike [runAndStream], this does not stream the process output to stdout. - /// It also unconditionally throws an error on a non-zero exit code. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// Returns the [io.ProcessResult] of running the [executable]. - Future runAndExitOnError( - String executable, - List args, { - Directory workingDir, - }) async { - final io.ProcessResult result = await io.Process.run(executable, args, - workingDirectory: workingDir?.path); - if (result.exitCode != 0) { - final String error = - _getErrorString(executable, args, workingDir: workingDir); - print('$error Stderr:\n${result.stdout}'); - throw ToolExit(result.exitCode); - } - return result; - } - - String _getErrorString(String executable, List args, - {Directory workingDir}) { - final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; - return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; - } -} diff --git a/packages/plugin_tools/lib/src/create_all_plugins_app_command.dart b/packages/plugin_tools/lib/src/create_all_plugins_app_command.dart deleted file mode 100644 index 0f1431c5aee0..000000000000 --- a/packages/plugin_tools/lib/src/create_all_plugins_app_command.dart +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; - -import 'common.dart'; - -// TODO(cyanglaz): Add tests for this command. -// https://github.com/flutter/flutter/issues/61049 -class CreateAllPluginsAppCommand extends PluginCommand { - CreateAllPluginsAppCommand(Directory packagesDir, FileSystem fileSystem) - : super(packagesDir, fileSystem); - - @override - String get description => - 'Generate Flutter app that includes all plugins in packages.'; - - @override - String get name => 'all-plugins-app'; - - @override - Future run() async { - final int exitCode = await _createPlugin(); - if (exitCode != 0) { - throw ToolExit(exitCode); - } - - await Future.wait(>[ - _genPubspecWithAllPlugins(), - _updateAppGradle(), - _updateManifest(), - ]); - } - - Future _createPlugin() async { - final io.ProcessResult result = io.Process.runSync( - 'flutter', - [ - 'create', - '--template=app', - '--project-name=all_plugins', - '--android-language=java', - './all_plugins', - ], - ); - - print(result.stdout); - print(result.stderr); - return result.exitCode; - } - - Future _updateAppGradle() async { - final File gradleFile = fileSystem.file(p.join( - 'all_plugins', - 'android', - 'app', - 'build.gradle', - )); - if (!gradleFile.existsSync()) { - throw ToolExit(64); - } - - final StringBuffer newGradle = StringBuffer(); - for (String line in gradleFile.readAsLinesSync()) { - newGradle.writeln(line); - if (line.contains('defaultConfig {')) { - newGradle.writeln(' multiDexEnabled true'); - } else if (line.contains('dependencies {')) { - newGradle.writeln( - ' implementation \'com.google.guava:guava:27.0.1-android\'\n', - ); - // Tests for https://github.com/flutter/flutter/issues/43383 - newGradle.writeln( - " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", - ); - } - } - gradleFile.writeAsStringSync(newGradle.toString()); - } - - Future _updateManifest() async { - final File manifestFile = fileSystem.file(p.join( - 'all_plugins', - 'android', - 'app', - 'src', - 'main', - 'AndroidManifest.xml', - )); - if (!manifestFile.existsSync()) { - throw ToolExit(64); - } - - final StringBuffer newManifest = StringBuffer(); - for (String line in manifestFile.readAsLinesSync()) { - if (line.contains('package="com.example.all_plugins"')) { - newManifest - ..writeln('package="com.example.all_plugins"') - ..writeln('xmlns:tools="http://schemas.android.com/tools">') - ..writeln() - ..writeln( - '', - ); - } else { - newManifest.writeln(line); - } - } - manifestFile.writeAsStringSync(newManifest.toString()); - } - - Future _genPubspecWithAllPlugins() async { - final Map pluginDeps = - await _getValidPathDependencies(); - final Pubspec pubspec = Pubspec( - 'all_plugins', - description: 'Flutter app containing all 1st party plugins.', - version: Version.parse('1.0.0+1'), - environment: { - 'sdk': VersionConstraint.compatibleWith( - Version.parse('2.0.0'), - ), - }, - dependencies: { - 'flutter': SdkDependency('flutter'), - }..addAll(pluginDeps), - devDependencies: { - 'flutter_test': SdkDependency('flutter'), - }, - dependencyOverrides: pluginDeps, - ); - final File pubspecFile = - fileSystem.file(p.join('all_plugins', 'pubspec.yaml')); - pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); - } - - Future> _getValidPathDependencies() async { - final Map pathDependencies = - {}; - - await for (Directory package in getPlugins()) { - final String pluginName = package.path.split('/').last; - final File pubspecFile = - fileSystem.file(p.join(package.path, 'pubspec.yaml')); - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); - - if (pubspec.publishTo != 'none') { - pathDependencies[pluginName] = PathDependency(package.path); - } - } - return pathDependencies; - } - - String _pubspecToString(Pubspec pubspec) { - return ''' -### Generated file. Do not edit. Run `pub global run flutter_plugin_tools gen-pubspec` to update. -name: ${pubspec.name} -description: ${pubspec.description} - -version: ${pubspec.version} - -environment:${_pubspecMapString(pubspec.environment)} - -dependencies:${_pubspecMapString(pubspec.dependencies)} - -dependency_overrides:${_pubspecMapString(pubspec.dependencyOverrides)} - -dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} -###'''; - } - - String _pubspecMapString(Map values) { - final StringBuffer buffer = StringBuffer(); - - for (MapEntry entry in values.entries) { - buffer.writeln(); - if (entry.value is VersionConstraint) { - buffer.write(' ${entry.key}: ${entry.value}'); - } else if (entry.value is SdkDependency) { - final SdkDependency dep = entry.value; - buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); - } else if (entry.value is PathDependency) { - final PathDependency dep = entry.value; - buffer.write(' ${entry.key}: \n path: ${dep.path}'); - } else { - throw UnimplementedError( - 'Not available for type: ${entry.value.runtimeType}', - ); - } - } - - return buffer.toString(); - } -} diff --git a/packages/plugin_tools/lib/src/drive_examples_command.dart b/packages/plugin_tools/lib/src/drive_examples_command.dart deleted file mode 100644 index 8b1fa3624fbf..000000000000 --- a/packages/plugin_tools/lib/src/drive_examples_command.dart +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'common.dart'; - -class DriveExamplesCommand extends PluginCommand { - DriveExamplesCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addFlag(kLinux, - help: 'Runs the Linux implementation of the examples'); - argParser.addFlag(kMacos, - help: 'Runs the macOS implementation of the examples'); - argParser.addFlag(kWindows, - help: 'Runs the Windows implementation of the examples'); - argParser.addFlag(kIos, - help: 'Runs the iOS implementation of the examples'); - argParser.addFlag(kAndroid, - help: 'Runs the Android implementation of the examples'); - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: - 'Runs the driver tests in Dart VM with the given experiments enabled.', - ); - } - - @override - final String name = 'drive-examples'; - - @override - final String description = 'Runs driver tests for plugin example apps.\n\n' - 'For each *_test.dart in test_driver/ it drives an application with a ' - 'corresponding name in the test/ or test_driver/ directories.\n\n' - 'For example, test_driver/app_test.dart would match test/app.dart.\n\n' - 'This command requires "flutter" to be in your path.\n\n' - 'If a file with a corresponding name cannot be found, this driver file' - 'will be used to drive the tests that match ' - 'integration_test/*_test.dart.'; - - @override - Future run() async { - checkSharding(); - final List failingTests = []; - final bool isLinux = argResults[kLinux]; - final bool isMacos = argResults[kMacos]; - final bool isWindows = argResults[kWindows]; - await for (Directory plugin in getPlugins()) { - final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - for (Directory example in getExamplesForPlugin(plugin)) { - final String packageName = - p.relative(example.path, from: packagesDir.path); - if (!(await pluginSupportedOnCurrentPlatform(plugin, fileSystem))) { - continue; - } - final Directory driverTests = - fileSystem.directory(p.join(example.path, 'test_driver')); - if (!driverTests.existsSync()) { - // No driver tests available for this example - continue; - } - // Look for driver tests ending in _test.dart in test_driver/ - await for (FileSystemEntity test in driverTests.list()) { - final String driverTestName = - p.relative(test.path, from: driverTests.path); - if (!driverTestName.endsWith('_test.dart')) { - continue; - } - // Try to find a matching app to drive without the _test.dart - final String deviceTestName = driverTestName.replaceAll( - RegExp(r'_test.dart$'), - '.dart', - ); - String deviceTestPath = p.join('test', deviceTestName); - if (!fileSystem - .file(p.join(example.path, deviceTestPath)) - .existsSync()) { - // If the app isn't in test/ folder, look in test_driver/ instead. - deviceTestPath = p.join('test_driver', deviceTestName); - } - - final List targetPaths = []; - if (fileSystem - .file(p.join(example.path, deviceTestPath)) - .existsSync()) { - targetPaths.add(deviceTestPath); - } else { - final Directory integrationTests = - fileSystem.directory(p.join(example.path, 'integration_test')); - - if (await integrationTests.exists()) { - await for (FileSystemEntity integration_test - in integrationTests.list()) { - if (!integration_test.basename.endsWith('_test.dart')) { - continue; - } - targetPaths - .add(p.relative(integration_test.path, from: example.path)); - } - } - - if (targetPaths.isEmpty) { - print(''' -Unable to infer a target application for $driverTestName to drive. -Tried searching for the following: -1. test/$deviceTestName -2. test_driver/$deviceTestName -3. test_driver/*_test.dart -'''); - failingTests.add(p.relative(test.path, from: example.path)); - continue; - } - } - - final List driveArgs = ['drive', '-v']; - - final String enableExperiment = argResults[kEnableExperiment]; - if (enableExperiment.isNotEmpty) { - driveArgs.add('--enable-experiment=$enableExperiment'); - } - - if (isLinux && isLinuxPlugin(plugin, fileSystem)) { - driveArgs.addAll([ - '-d', - 'linux', - ]); - } - if (isMacos && isMacOsPlugin(plugin, fileSystem)) { - driveArgs.addAll([ - '-d', - 'macos', - ]); - } - if (isWindows && isWindowsPlugin(plugin, fileSystem)) { - driveArgs.addAll([ - '-d', - 'windows', - ]); - } - - for (final targetPath in targetPaths) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - ...driveArgs, - '--driver', - p.join('test_driver', driverTestName), - '--target', - targetPath, - ], - workingDir: example, - exitOnError: true); - if (exitCode != 0) { - failingTests.add(p.join(packageName, deviceTestPath)); - } - } - } - } - } - print('\n\n'); - - if (failingTests.isNotEmpty) { - print('The following driver tests are failing (see above for details):'); - for (String test in failingTests) { - print(' * $test'); - } - throw ToolExit(1); - } - - print('All driver tests successful!'); - } - - Future pluginSupportedOnCurrentPlatform( - FileSystemEntity plugin, FileSystem fileSystem) async { - final bool isLinux = argResults[kLinux]; - final bool isMacos = argResults[kMacos]; - final bool isWindows = argResults[kWindows]; - final bool isIOS = argResults[kIos]; - final bool isAndroid = argResults[kAndroid]; - if (isLinux) { - return isLinuxPlugin(plugin, fileSystem); - } - if (isMacos) { - return isMacOsPlugin(plugin, fileSystem); - } - if (isWindows) { - return isWindowsPlugin(plugin, fileSystem); - } - if (isIOS) { - return isIosPlugin(plugin, fileSystem); - } - if (isAndroid) { - return (isAndroidPlugin(plugin, fileSystem)); - } - // When we are here, no flags are specified. Only return true if the plugin supports mobile for legacy command support. - // TODO(cyanglaz): Make mobile platforms flags also required like other platforms (breaking change). - // https://github.com/flutter/flutter/issues/58285 - final bool isMobilePlugin = - isIosPlugin(plugin, fileSystem) || isAndroidPlugin(plugin, fileSystem); - return isMobilePlugin; - } -} diff --git a/packages/plugin_tools/lib/src/firebase_test_lab_command.dart b/packages/plugin_tools/lib/src/firebase_test_lab_command.dart deleted file mode 100644 index 0b4b2a471dbc..000000000000 --- a/packages/plugin_tools/lib/src/firebase_test_lab_command.dart +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:uuid/uuid.dart'; - -import 'common.dart'; - -class FirebaseTestLabCommand extends PluginCommand { - FirebaseTestLabCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - Print print = print, - }) : _print = print, - super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption( - 'project', - defaultsTo: 'flutter-infra', - help: 'The Firebase project name.', - ); - argParser.addOption('service-key', - defaultsTo: - p.join(io.Platform.environment['HOME'], 'gcloud-service-key.json')); - argParser.addOption('test-run-id', - defaultsTo: Uuid().v4(), - help: - 'Optional string to append to the results path, to avoid conflicts. ' - 'Randomly chosen on each invocation if none is provided. ' - 'The default shown here is just an example.'); - argParser.addMultiOption('device', - splitCommas: false, - defaultsTo: [ - 'model=walleye,version=26', - 'model=flame,version=29' - ], - help: - 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); - argParser.addOption('results-bucket', - defaultsTo: 'gs://flutter_firebase_testlab'); - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: 'Enables the given Dart SDK experiments.', - ); - } - - @override - final String name = 'firebase-test-lab'; - - @override - final String description = 'Runs the instrumentation tests of the example ' - 'apps on Firebase Test Lab.\n\n' - 'Runs tests in test_instrumentation folder using the ' - 'instrumentation_test package.'; - - static const String _gradleWrapper = 'gradlew'; - - final Print _print; - - Completer _firebaseProjectConfigured; - - Future _configureFirebaseProject() async { - if (_firebaseProjectConfigured != null) { - return _firebaseProjectConfigured.future; - } else { - _firebaseProjectConfigured = Completer(); - } - await processRunner.runAndExitOnError('gcloud', [ - 'auth', - 'activate-service-account', - '--key-file=${argResults['service-key']}', - ]); - int exitCode = await processRunner.runAndStream('gcloud', [ - 'config', - 'set', - 'project', - argResults['project'], - ]); - if (exitCode == 0) { - _print('\nFirebase project configured.'); - return; - } else { - _print( - '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.'); - } - _firebaseProjectConfigured.complete(null); - } - - @override - Future run() async { - checkSharding(); - final Stream packagesWithTests = getPackages().where( - (Directory d) => - isFlutterPackage(d, fileSystem) && - fileSystem - .directory(p.join( - d.path, 'example', 'android', 'app', 'src', 'androidTest')) - .existsSync()); - - final List failingPackages = []; - final List missingFlutterBuild = []; - int resultsCounter = - 0; // We use a unique GCS bucket for each Firebase Test Lab run - await for (Directory package in packagesWithTests) { - // See https://github.com/flutter/flutter/issues/38983 - - final Directory exampleDirectory = - fileSystem.directory(p.join(package.path, 'example')); - final String packageName = - p.relative(package.path, from: packagesDir.path); - _print('\nRUNNING FIREBASE TEST LAB TESTS for $packageName'); - - final Directory androidDirectory = - fileSystem.directory(p.join(exampleDirectory.path, 'android')); - - final String enableExperiment = argResults[kEnableExperiment]; - final String encodedEnableExperiment = - Uri.encodeComponent('--enable-experiment=$enableExperiment'); - - // Ensures that gradle wrapper exists - if (!fileSystem - .file(p.join(androidDirectory.path, _gradleWrapper)) - .existsSync()) { - final int exitCode = await processRunner.runAndStream( - 'flutter', - [ - 'build', - 'apk', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: androidDirectory); - - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - continue; - } - - await _configureFirebaseProject(); - - int exitCode = await processRunner.runAndStream( - p.join(androidDirectory.path, _gradleWrapper), - [ - 'app:assembleAndroidTest', - '-Pverbose=true', - if (enableExperiment.isNotEmpty) - '-Pextra-front-end-options=$encodedEnableExperiment', - if (enableExperiment.isNotEmpty) - '-Pextra-gen-snapshot-options=$encodedEnableExperiment', - ], - workingDir: androidDirectory); - - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - - // Look for tests recursively in folders that start with 'test' and that - // live in the root or example folders. - bool isTestDir(FileSystemEntity dir) { - return p.basename(dir.path).startsWith('test') || - p.basename(dir.path) == 'integration_test'; - } - - final List testDirs = - package.listSync().where(isTestDir).toList(); - final Directory example = - fileSystem.directory(p.join(package.path, 'example')); - testDirs.addAll(example.listSync().where(isTestDir).toList()); - for (Directory testDir in testDirs) { - bool isE2ETest(FileSystemEntity file) { - return file.path.endsWith('_e2e.dart') || - (file.parent.basename == 'integration_test' && - file.path.endsWith('_test.dart')); - } - - final List testFiles = testDir - .listSync(recursive: true, followLinks: true) - .where(isE2ETest) - .toList(); - for (FileSystemEntity test in testFiles) { - exitCode = await processRunner.runAndStream( - p.join(androidDirectory.path, _gradleWrapper), - [ - 'app:assembleDebug', - '-Pverbose=true', - '-Ptarget=${test.path}', - if (enableExperiment.isNotEmpty) - '-Pextra-front-end-options=$encodedEnableExperiment', - if (enableExperiment.isNotEmpty) - '-Pextra-gen-snapshot-options=$encodedEnableExperiment', - ], - workingDir: androidDirectory); - - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - final String buildId = io.Platform.environment['CIRRUS_BUILD_ID']; - final String testRunId = argResults['test-run-id']; - final String resultsDir = - 'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/'; - final List args = [ - 'firebase', - 'test', - 'android', - 'run', - '--type', - 'instrumentation', - '--app', - 'build/app/outputs/apk/debug/app-debug.apk', - '--test', - 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', - '--timeout', - '5m', - '--results-bucket=${argResults['results-bucket']}', - '--results-dir=${resultsDir}', - ]; - for (String device in argResults['device']) { - args.addAll(['--device', device]); - } - exitCode = await processRunner.runAndStream('gcloud', args, - workingDir: exampleDirectory); - - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - } - } - } - - _print('\n\n'); - if (failingPackages.isNotEmpty) { - _print( - 'The instrumentation tests for the following packages are failing (see above for' - 'details):'); - for (String package in failingPackages) { - _print(' * $package'); - } - } - if (missingFlutterBuild.isNotEmpty) { - _print('Run "pub global run flutter_plugin_tools build-examples --apk" on' - 'the following packages before executing tests again:'); - for (String package in missingFlutterBuild) { - _print(' * $package'); - } - } - - if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { - throw ToolExit(1); - } - - _print('All Firebase Test Lab tests successful!'); - } -} diff --git a/packages/plugin_tools/lib/src/format_command.dart b/packages/plugin_tools/lib/src/format_command.dart deleted file mode 100644 index ec326b96c1f9..000000000000 --- a/packages/plugin_tools/lib/src/format_command.dart +++ /dev/null @@ -1,147 +0,0 @@ -// 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 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as p; -import 'package:quiver/iterables.dart'; - -import 'common.dart'; - -const String _googleFormatterUrl = - 'https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'; - -class FormatCommand extends PluginCommand { - FormatCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addFlag('travis', hide: true); - argParser.addOption('clang-format', - defaultsTo: 'clang-format', - help: 'Path to executable of clang-format v5.'); - } - - @override - final String name = 'format'; - - @override - final String description = - 'Formats the code of all packages (Java, Objective-C, C++, and Dart).\n\n' - 'This command requires "git", "flutter" and "clang-format" v5 to be in ' - 'your path.'; - - @override - Future run() async { - checkSharding(); - final String googleFormatterPath = await _getGoogleFormatterPath(); - - await _formatDart(); - await _formatJava(googleFormatterPath); - await _formatCppAndObjectiveC(); - - if (argResults['travis']) { - final bool modified = await _didModifyAnything(); - if (modified) { - throw ToolExit(1); - } - } - } - - Future _didModifyAnything() async { - final io.ProcessResult modifiedFiles = await processRunner - .runAndExitOnError('git', ['ls-files', '--modified'], - workingDir: packagesDir); - - print('\n\n'); - - if (modifiedFiles.stdout.isEmpty) { - print('All files formatted correctly.'); - return false; - } - - print('These files are not formatted correctly (see diff below):'); - LineSplitter.split(modifiedFiles.stdout) - .map((String line) => ' $line') - .forEach(print); - - print('\nTo fix run "pub global activate flutter_plugin_tools && ' - 'pub global run flutter_plugin_tools format" or copy-paste ' - 'this command into your terminal:'); - - print('patch -p1 <['diff'], workingDir: packagesDir); - print(diff.stdout); - print('DONE'); - return true; - } - - Future _formatCppAndObjectiveC() async { - print('Formatting all .cc, .cpp, .mm, .m, and .h files...'); - final Iterable allFiles = [] - ..addAll(await _getFilesWithExtension('.h')) - ..addAll(await _getFilesWithExtension('.m')) - ..addAll(await _getFilesWithExtension('.mm')) - ..addAll(await _getFilesWithExtension('.cc')) - ..addAll(await _getFilesWithExtension('.cpp')); - // Split this into multiple invocations to avoid a - // 'ProcessException: Argument list too long'. - final Iterable> batches = partition(allFiles, 100); - for (List batch in batches) { - await processRunner.runAndStream(argResults['clang-format'], - ['-i', '--style=Google']..addAll(batch), - workingDir: packagesDir, exitOnError: true); - } - } - - Future _formatJava(String googleFormatterPath) async { - print('Formatting all .java files...'); - final Iterable javaFiles = await _getFilesWithExtension('.java'); - await processRunner.runAndStream('java', - ['-jar', googleFormatterPath, '--replace']..addAll(javaFiles), - workingDir: packagesDir, exitOnError: true); - } - - Future _formatDart() async { - // This actually should be fine for non-Flutter Dart projects, no need to - // specifically shell out to dartfmt -w in that case. - print('Formatting all .dart files...'); - final Iterable dartFiles = await _getFilesWithExtension('.dart'); - if (dartFiles.isEmpty) { - print( - 'No .dart files to format. If you set the `--exclude` flag, most likey they were skipped'); - } else { - await processRunner.runAndStream( - 'flutter', ['format']..addAll(dartFiles), - workingDir: packagesDir, exitOnError: true); - } - } - - Future> _getFilesWithExtension(String extension) async => - getFiles() - .where((File file) => p.extension(file.path) == extension) - .map((File file) => file.path) - .toList(); - - Future _getGoogleFormatterPath() async { - final String javaFormatterPath = p.join( - p.dirname(p.fromUri(io.Platform.script)), - 'google-java-format-1.3-all-deps.jar'); - final File javaFormatterFile = fileSystem.file(javaFormatterPath); - - if (!javaFormatterFile.existsSync()) { - print('Downloading Google Java Format...'); - final http.Response response = await http.get(_googleFormatterUrl); - javaFormatterFile.writeAsBytesSync(response.bodyBytes); - } - - return javaFormatterPath; - } -} diff --git a/packages/plugin_tools/lib/src/java_test_command.dart b/packages/plugin_tools/lib/src/java_test_command.dart deleted file mode 100644 index cf605bfc5ce2..000000000000 --- a/packages/plugin_tools/lib/src/java_test_command.dart +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; - -import 'common.dart'; - -class JavaTestCommand extends PluginCommand { - JavaTestCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner); - - @override - final String name = 'java-test'; - - @override - final String description = 'Runs the Java tests of the example apps.\n\n' - 'Building the apks of the example apps is required before executing this' - 'command.'; - - static const String _gradleWrapper = 'gradlew'; - - @override - Future run() async { - checkSharding(); - final Stream examplesWithTests = getExamples().where( - (Directory d) => - isFlutterPackage(d, fileSystem) && - fileSystem - .directory(p.join(d.path, 'android', 'app', 'src', 'test')) - .existsSync()); - - final List failingPackages = []; - final List missingFlutterBuild = []; - await for (Directory example in examplesWithTests) { - final String packageName = - p.relative(example.path, from: packagesDir.path); - print('\nRUNNING JAVA TESTS for $packageName'); - - final Directory androidDirectory = - fileSystem.directory(p.join(example.path, 'android')); - if (!fileSystem - .file(p.join(androidDirectory.path, _gradleWrapper)) - .existsSync()) { - print('ERROR: Run "flutter build apk" on example app of $packageName' - 'before executing tests.'); - missingFlutterBuild.add(packageName); - continue; - } - - final int exitCode = await processRunner.runAndStream( - p.join(androidDirectory.path, _gradleWrapper), - ['testDebugUnitTest', '--info'], - workingDir: androidDirectory); - if (exitCode != 0) { - failingPackages.add(packageName); - } - } - - print('\n\n'); - if (failingPackages.isNotEmpty) { - print( - 'The Java tests for the following packages are failing (see above for' - 'details):'); - for (String package in failingPackages) { - print(' * $package'); - } - } - if (missingFlutterBuild.isNotEmpty) { - print('Run "pub global run flutter_plugin_tools build-examples --apk" on' - 'the following packages before executing tests again:'); - for (String package in missingFlutterBuild) { - print(' * $package'); - } - } - - if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { - throw ToolExit(1); - } - - print('All Java tests successful!'); - } -} diff --git a/packages/plugin_tools/lib/src/lint_podspecs_command.dart b/packages/plugin_tools/lib/src/lint_podspecs_command.dart deleted file mode 100644 index 68fd4b61dd66..000000000000 --- a/packages/plugin_tools/lib/src/lint_podspecs_command.dart +++ /dev/null @@ -1,146 +0,0 @@ -// 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 'dart:convert'; -import 'dart:io'; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; - -import 'common.dart'; - -typedef void Print(Object object); - -/// Lint the CocoaPod podspecs, run the static analyzer on iOS/macOS plugin -/// platform code, and run unit tests. -/// -/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. -class LintPodspecsCommand extends PluginCommand { - LintPodspecsCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - this.platform = const LocalPlatform(), - Print print = print, - }) : _print = print, - super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addMultiOption('skip', - help: - 'Skip all linting for podspecs with this basename (example: federated plugins with placeholder podspecs)', - valueHelp: 'podspec_file_name'); - argParser.addMultiOption('ignore-warnings', - help: - 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs with this basename (example: plugins with known warnings)', - valueHelp: 'podspec_file_name'); - argParser.addMultiOption('no-analyze', - help: - 'Do not pass --analyze flag to "pod lib lint" for podspecs with this basename (example: plugins with known analyzer warnings)', - valueHelp: 'podspec_file_name'); - } - - @override - final String name = 'podspecs'; - - @override - List get aliases => ['podspec']; - - @override - final String description = - 'Runs "pod lib lint" on all iOS and macOS plugin podspecs.\n\n' - 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; - - final Platform platform; - - final Print _print; - - @override - Future run() async { - if (!platform.isMacOS) { - _print('Detected platform is not macOS, skipping podspec lint'); - return; - } - - checkSharding(); - - await processRunner.runAndExitOnError('which', ['pod'], - workingDir: packagesDir); - - _print('Starting podspec lint test'); - - final List failingPlugins = []; - for (File podspec in await _podspecsToLint()) { - if (!await _lintPodspec(podspec)) { - failingPlugins.add(p.basenameWithoutExtension(podspec.path)); - } - } - - _print('\n\n'); - if (failingPlugins.isNotEmpty) { - _print('The following plugins have podspec errors (see above):'); - failingPlugins.forEach((String plugin) { - _print(' * $plugin'); - }); - throw ToolExit(1); - } - } - - Future> _podspecsToLint() async { - final List podspecs = await getFiles().where((File entity) { - final String filePath = entity.path; - return p.extension(filePath) == '.podspec' && - !argResults['skip'].contains(p.basenameWithoutExtension(filePath)); - }).toList(); - - podspecs.sort( - (File a, File b) => p.basename(a.path).compareTo(p.basename(b.path))); - return podspecs; - } - - Future _lintPodspec(File podspec) async { - // Do not run the static analyzer on plugins with known analyzer issues. - final String podspecPath = podspec.path; - final bool runAnalyzer = !argResults['no-analyze'] - .contains(p.basenameWithoutExtension(podspecPath)); - - final String podspecBasename = p.basename(podspecPath); - if (runAnalyzer) { - _print('Linting and analyzing $podspecBasename'); - } else { - _print('Linting $podspecBasename'); - } - - // Lint plugin as framework (use_frameworks!). - final ProcessResult frameworkResult = await _runPodLint(podspecPath, - runAnalyzer: runAnalyzer, libraryLint: true); - _print(frameworkResult.stdout); - _print(frameworkResult.stderr); - - // Lint plugin as library. - final ProcessResult libraryResult = await _runPodLint(podspecPath, - runAnalyzer: runAnalyzer, libraryLint: false); - _print(libraryResult.stdout); - _print(libraryResult.stderr); - - return frameworkResult.exitCode == 0 && libraryResult.exitCode == 0; - } - - Future _runPodLint(String podspecPath, - {bool runAnalyzer, bool libraryLint}) async { - final bool allowWarnings = argResults['ignore-warnings'] - .contains(p.basenameWithoutExtension(podspecPath)); - final List arguments = [ - 'lib', - 'lint', - podspecPath, - if (allowWarnings) '--allow-warnings', - if (runAnalyzer) '--analyze', - if (libraryLint) '--use-libraries' - ]; - - return processRunner.run('pod', arguments, - workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); - } -} diff --git a/packages/plugin_tools/lib/src/list_command.dart b/packages/plugin_tools/lib/src/list_command.dart deleted file mode 100644 index 7f94daac7096..000000000000 --- a/packages/plugin_tools/lib/src/list_command.dart +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:file/file.dart'; - -import 'common.dart'; - -class ListCommand extends PluginCommand { - ListCommand(Directory packagesDir, FileSystem fileSystem) - : super(packagesDir, fileSystem) { - argParser.addOption( - _type, - defaultsTo: _plugin, - allowed: [_plugin, _example, _package, _file], - help: 'What type of file system content to list.', - ); - } - - static const String _type = 'type'; - static const String _plugin = 'plugin'; - static const String _example = 'example'; - static const String _package = 'package'; - static const String _file = 'file'; - - @override - final String name = 'list'; - - @override - final String description = 'Lists packages or files'; - - @override - Future run() async { - checkSharding(); - switch (argResults[_type]) { - case _plugin: - await for (Directory package in getPlugins()) { - print(package.path); - } - break; - case _example: - await for (Directory package in getExamples()) { - print(package.path); - } - break; - case _package: - await for (Directory package in getPackages()) { - print(package.path); - } - break; - case _file: - await for (File file in getFiles()) { - print(file.path); - } - break; - } - } -} diff --git a/packages/plugin_tools/lib/src/main.dart b/packages/plugin_tools/lib/src/main.dart deleted file mode 100644 index bb3f67c0a9e1..000000000000 --- a/packages/plugin_tools/lib/src/main.dart +++ /dev/null @@ -1,63 +0,0 @@ -// 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:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; -import 'package:path/path.dart' as p; - -import 'analyze_command.dart'; -import 'build_examples_command.dart'; -import 'common.dart'; -import 'create_all_plugins_app_command.dart'; -import 'drive_examples_command.dart'; -import 'firebase_test_lab_command.dart'; -import 'format_command.dart'; -import 'java_test_command.dart'; -import 'lint_podspecs_command.dart'; -import 'list_command.dart'; -import 'test_command.dart'; -import 'version_check_command.dart'; -import 'xctest_command.dart'; - -void main(List args) { - final FileSystem fileSystem = const LocalFileSystem(); - - Directory packagesDir = fileSystem - .directory(p.join(fileSystem.currentDirectory.path, 'packages')); - - if (!packagesDir.existsSync()) { - if (p.basename(fileSystem.currentDirectory.path) == 'packages') { - packagesDir = fileSystem.currentDirectory; - } else { - print('Error: Cannot find a "packages" sub-directory'); - io.exit(1); - } - } - - final CommandRunner commandRunner = CommandRunner( - 'pub global run flutter_plugin_tools', - 'Productivity utils for hosting multiple plugins within one repository.') - ..addCommand(AnalyzeCommand(packagesDir, fileSystem)) - ..addCommand(BuildExamplesCommand(packagesDir, fileSystem)) - ..addCommand(CreateAllPluginsAppCommand(packagesDir, fileSystem)) - ..addCommand(DriveExamplesCommand(packagesDir, fileSystem)) - ..addCommand(FirebaseTestLabCommand(packagesDir, fileSystem)) - ..addCommand(FormatCommand(packagesDir, fileSystem)) - ..addCommand(JavaTestCommand(packagesDir, fileSystem)) - ..addCommand(LintPodspecsCommand(packagesDir, fileSystem)) - ..addCommand(ListCommand(packagesDir, fileSystem)) - ..addCommand(PublishPluginCommand(packagesDir, fileSystem)) - ..addCommand(TestCommand(packagesDir, fileSystem)) - ..addCommand(VersionCheckCommand(packagesDir, fileSystem)) - ..addCommand(XCTestCommand(packagesDir, fileSystem)); - - commandRunner.run(args).catchError((Object e) { - final ToolExit toolExit = e; - io.exit(toolExit.exitCode); - }, test: (Object e) => e is ToolExit); -} diff --git a/packages/plugin_tools/lib/src/publish_plugin_command.dart b/packages/plugin_tools/lib/src/publish_plugin_command.dart deleted file mode 100644 index 55c48f5484e5..000000000000 --- a/packages/plugin_tools/lib/src/publish_plugin_command.dart +++ /dev/null @@ -1,227 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; -import 'package:yaml/yaml.dart'; - -import 'common.dart'; - -/// Wraps pub publish with a few niceties used by the flutter/plugin team. -/// -/// 1. Checks for any modified files in git and refuses to publish if there's an -/// issue. -/// 2. Tags the release with the format -v. -/// 3. Pushes the release to a remote. -/// -/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full -/// usage information. -/// -/// [processRunner], [print], and [stdin] can be overriden for easier testing. -class PublishPluginCommand extends PluginCommand { - PublishPluginCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - Print print = print, - Stdin stdinput, - }) : _print = print, - _stdin = stdinput ?? stdin, - super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption( - _packageOption, - help: 'The package to publish.' - 'If the package directory name is different than its pubspec.yaml name, then this should specify the directory.', - ); - argParser.addMultiOption(_pubFlagsOption, - help: - 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); - argParser.addFlag( - _tagReleaseOption, - help: 'Whether or not to tag the release.', - defaultsTo: true, - negatable: true, - ); - argParser.addFlag( - _pushTagsOption, - help: - 'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.', - defaultsTo: true, - negatable: true, - ); - argParser.addOption( - _remoteOption, - help: - 'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.', - // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. - defaultsTo: 'upstream', - ); - } - - static const String _packageOption = 'package'; - static const String _tagReleaseOption = 'tag-release'; - static const String _pushTagsOption = 'push-tags'; - static const String _pubFlagsOption = 'pub-publish-flags'; - static const String _remoteOption = 'remote'; - - // Version tags should follow -v. For example, - // `flutter_plugin_tools-v0.0.24`. - static const String _tagFormat = '%PACKAGE%-v%VERSION%'; - - @override - final String name = 'publish-plugin'; - - @override - final String description = - 'Attempts to publish the given plugin and tag its release on GitHub.'; - - final Print _print; - final Stdin _stdin; - // The directory of the actual package that we are publishing. - Directory _packageDir; - StreamSubscription _stdinSubscription; - - @override - Future run() async { - checkSharding(); - _print('Checking local repo...'); - _packageDir = _checkPackageDir(); - await _checkGitStatus(); - final bool shouldPushTag = argResults[_pushTagsOption]; - final String remote = argResults[_remoteOption]; - String remoteUrl; - if (shouldPushTag) { - remoteUrl = await _verifyRemote(remote); - } - _print('Local repo is ready!'); - - await _publish(); - _print('Package published!'); - if (!argResults[_tagReleaseOption]) { - return await _finishSuccesfully(); - } - - _print('Tagging release...'); - final String tag = _getTag(); - await processRunner.runAndExitOnError('git', ['tag', tag], - workingDir: _packageDir); - if (!shouldPushTag) { - return await _finishSuccesfully(); - } - - _print('Pushing tag to $remote...'); - await _pushTagToRemote(remote: remote, tag: tag, remoteUrl: remoteUrl); - await _finishSuccesfully(); - } - - Future _finishSuccesfully() async { - await _stdinSubscription.cancel(); - _print('Done!'); - } - - Directory _checkPackageDir() { - final String package = argResults[_packageOption]; - if (package == null) { - _print( - 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); - throw ToolExit(1); - } - final Directory _packageDir = packagesDir.childDirectory(package); - if (!_packageDir.existsSync()) { - _print('${_packageDir.absolute.path} does not exist.'); - throw ToolExit(1); - } - if (!isFlutterPackage(_packageDir, fileSystem)) { - _print('${_packageDir.absolute.path} is not a flutter package.'); - throw ToolExit(1); - } - return _packageDir; - } - - Future _checkGitStatus() async { - if (!await GitDir.isGitDir(packagesDir.path)) { - _print('$packagesDir is not a valid Git repository.'); - throw ToolExit(1); - } - - final ProcessResult statusResult = await processRunner.runAndExitOnError( - 'git', - [ - 'status', - '--porcelain', - '--ignored', - _packageDir.absolute.path - ], - workingDir: _packageDir); - final String statusOutput = statusResult.stdout; - if (statusOutput.isNotEmpty) { - _print( - "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" - '$statusOutput\n' - 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); - throw ToolExit(1); - } - } - - Future _verifyRemote(String remote) async { - final ProcessResult remoteInfo = await processRunner.runAndExitOnError( - 'git', ['remote', 'get-url', remote], - workingDir: _packageDir); - return remoteInfo.stdout; - } - - Future _publish() async { - final List publishFlags = argResults[_pubFlagsOption]; - _print( - 'Running `pub publish ${publishFlags.join(' ')}` in ${_packageDir.absolute.path}...\n'); - final Process publish = await processRunner.start( - 'flutter', ['pub', 'publish'] + publishFlags, - workingDirectory: _packageDir); - publish.stdout - .transform(utf8.decoder) - .listen((String data) => _print(data)); - publish.stderr - .transform(utf8.decoder) - .listen((String data) => _print(data)); - _stdinSubscription = _stdin - .transform(utf8.decoder) - .listen((String data) => publish.stdin.writeln(data)); - final int result = await publish.exitCode; - if (result != 0) { - _print('Publish failed. Exiting.'); - throw ToolExit(result); - } - } - - String _getTag() { - final File pubspecFile = - fileSystem.file(p.join(_packageDir.path, 'pubspec.yaml')); - final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); - final String name = pubspecYaml['name']; - final String version = pubspecYaml['version']; - // We should have failed to publish if these were unset. - assert(name.isNotEmpty && version.isNotEmpty); - return _tagFormat - .replaceAll('%PACKAGE%', name) - .replaceAll('%VERSION%', version); - } - - Future _pushTagToRemote( - {@required String remote, - @required String tag, - @required String remoteUrl}) async { - assert(remote != null && tag != null && remoteUrl != null); - _print('Ready to push $tag to $remoteUrl (y/n)?'); - final String input = _stdin.readLineSync(); - if (input.toLowerCase() != 'y') { - _print('Tag push canceled.'); - throw ToolExit(1); - } - - await processRunner.runAndExitOnError('git', ['push', remote, tag], - workingDir: packagesDir); - } -} diff --git a/packages/plugin_tools/lib/src/test_command.dart b/packages/plugin_tools/lib/src/test_command.dart deleted file mode 100644 index e938168cfa89..000000000000 --- a/packages/plugin_tools/lib/src/test_command.dart +++ /dev/null @@ -1,101 +0,0 @@ -// 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:file/file.dart'; -import 'package:path/path.dart' as p; - -import 'common.dart'; - -class TestCommand extends PluginCommand { - TestCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: 'Runs the tests in Dart VM with the given experiments enabled.', - ); - } - - @override - final String name = 'test'; - - @override - final String description = 'Runs the Dart tests for all packages.\n\n' - 'This command requires "flutter" to be in your path.'; - - @override - Future run() async { - checkSharding(); - final List failingPackages = []; - await for (Directory packageDir in getPackages()) { - final String packageName = - p.relative(packageDir.path, from: packagesDir.path); - if (!fileSystem.directory(p.join(packageDir.path, 'test')).existsSync()) { - print('SKIPPING $packageName - no test subdirectory'); - continue; - } - - print('RUNNING $packageName tests...'); - - final String enableExperiment = argResults[kEnableExperiment]; - - // `flutter test` automatically gets packages. `pub run test` does not. :( - int exitCode = 0; - if (isFlutterPackage(packageDir, fileSystem)) { - final List args = [ - 'test', - '--color', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ]; - - if (isWebPlugin(packageDir, fileSystem)) { - args.add('--platform=chrome'); - } - exitCode = await processRunner.runAndStream( - 'flutter', - args, - workingDir: packageDir, - ); - } else { - exitCode = await processRunner.runAndStream( - 'pub', - ['get'], - workingDir: packageDir, - ); - if (exitCode == 0) { - exitCode = await processRunner.runAndStream( - 'pub', - [ - 'run', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - 'test', - ], - workingDir: packageDir, - ); - } - } - if (exitCode != 0) { - failingPackages.add(packageName); - } - } - - print('\n\n'); - if (failingPackages.isNotEmpty) { - print('Tests for the following packages are failing (see above):'); - failingPackages.forEach((String package) { - print(' * $package'); - }); - throw ToolExit(1); - } - - print('All tests are passing!'); - } -} diff --git a/packages/plugin_tools/lib/src/version_check_command.dart b/packages/plugin_tools/lib/src/version_check_command.dart deleted file mode 100644 index 2c6b92bbcb7a..000000000000 --- a/packages/plugin_tools/lib/src/version_check_command.dart +++ /dev/null @@ -1,220 +0,0 @@ -// 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 'dart:io' as io; - -import 'package:meta/meta.dart'; -import 'package:colorize/colorize.dart'; -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; -import 'package:yaml/yaml.dart'; - -import 'common.dart'; - -const String _kBaseSha = 'base_sha'; - -class GitVersionFinder { - GitVersionFinder(this.baseGitDir, this.baseSha); - - final GitDir baseGitDir; - final String baseSha; - - static bool isPubspec(String file) { - return file.trim().endsWith('pubspec.yaml'); - } - - Future> getChangedPubSpecs() async { - final io.ProcessResult changedFilesCommand = await baseGitDir - .runCommand(['diff', '--name-only', '$baseSha', 'HEAD']); - final List changedFiles = - changedFilesCommand.stdout.toString().split('\n'); - return changedFiles.where(isPubspec).toList(); - } - - Future getPackageVersion(String pubspecPath, String gitRef) async { - final io.ProcessResult gitShow = - await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); - final String fileContent = gitShow.stdout; - final String versionString = loadYaml(fileContent)['version']; - return versionString == null ? null : Version.parse(versionString); - } -} - -enum NextVersionType { - BREAKING_MAJOR, - MAJOR_NULLSAFETY_PRE_RELEASE, - MINOR_NULLSAFETY_PRE_RELEASE, - MINOR, - PATCH, - RELEASE, -} - -Version getNextNullSafetyPreRelease(Version current, Version next) { - String nextNullsafetyPrerelease = 'nullsafety'; - if (current.isPreRelease && - current.preRelease.first is String && - current.preRelease.first == 'nullsafety') { - if (current.preRelease.length == 1) { - nextNullsafetyPrerelease = 'nullsafety.1'; - } else if (current.preRelease.length == 2 && - current.preRelease.last is int) { - nextNullsafetyPrerelease = 'nullsafety.${current.preRelease.last + 1}'; - } - } - return Version( - next.major, - next.minor, - next.patch, - pre: nextNullsafetyPrerelease, - ); -} - -@visibleForTesting -Map getAllowedNextVersions( - Version masterVersion, Version headVersion) { - final Version nextNullSafetyMajor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMajor); - final Version nextNullSafetyMinor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); - final Map allowedNextVersions = - { - masterVersion.nextMajor: NextVersionType.BREAKING_MAJOR, - nextNullSafetyMajor: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE, - nextNullSafetyMinor: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE, - masterVersion.nextMinor: NextVersionType.MINOR, - masterVersion.nextPatch: NextVersionType.PATCH, - }; - - if (masterVersion.major < 1 && headVersion.major < 1) { - int nextBuildNumber = -1; - if (masterVersion.build.isEmpty) { - nextBuildNumber = 1; - } else { - final int currentBuildNumber = masterVersion.build.first; - nextBuildNumber = currentBuildNumber + 1; - } - final Version preReleaseVersion = Version( - masterVersion.major, - masterVersion.minor, - masterVersion.patch, - build: nextBuildNumber.toString(), - ); - allowedNextVersions.clear(); - allowedNextVersions[masterVersion.nextMajor] = NextVersionType.RELEASE; - allowedNextVersions[masterVersion.nextMinor] = - NextVersionType.BREAKING_MAJOR; - allowedNextVersions[masterVersion.nextPatch] = NextVersionType.MINOR; - allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; - - final Version nextNullSafetyMajor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); - final Version nextNullSafetyMinor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextPatch); - - allowedNextVersions[nextNullSafetyMajor] = - NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE; - allowedNextVersions[nextNullSafetyMinor] = - NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE; - } - return allowedNextVersions; -} - -class VersionCheckCommand extends PluginCommand { - VersionCheckCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - this.gitDir, - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption(_kBaseSha); - } - - /// The git directory to use. By default it uses the parent directory. - /// - /// This can be mocked for testing. - final GitDir gitDir; - - @override - final String name = 'version-check'; - - @override - final String description = - 'Checks if the versions of the plugins have been incremented per pub specification.\n\n' - 'This command requires "pub" and "flutter" to be in your path.'; - - @override - Future run() async { - checkSharding(); - - final String rootDir = packagesDir.parent.absolute.path; - final String baseSha = argResults[_kBaseSha]; - - GitDir baseGitDir = gitDir; - if (baseGitDir == null) { - if (!await GitDir.isGitDir(rootDir)) { - print('$rootDir is not a valid Git repository.'); - throw ToolExit(2); - } - baseGitDir = await GitDir.fromExisting(rootDir); - } - - final GitVersionFinder gitVersionFinder = - GitVersionFinder(baseGitDir, baseSha); - - final List changedPubspecs = - await gitVersionFinder.getChangedPubSpecs(); - - for (final String pubspecPath in changedPubspecs) { - try { - final File pubspecFile = fileSystem.file(pubspecPath); - if (!pubspecFile.existsSync()) { - continue; - } - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); - if (pubspec.publishTo == 'none') { - continue; - } - - final Version masterVersion = - await gitVersionFinder.getPackageVersion(pubspecPath, baseSha); - final Version headVersion = - await gitVersionFinder.getPackageVersion(pubspecPath, 'HEAD'); - if (headVersion == null) { - continue; // Example apps don't have versions - } - - final Map allowedNextVersions = - getAllowedNextVersions(masterVersion, headVersion); - - if (!allowedNextVersions.containsKey(headVersion)) { - final String error = '$pubspecPath incorrectly updated version.\n' - 'HEAD: $headVersion, master: $masterVersion.\n' - 'Allowed versions: $allowedNextVersions'; - final Colorize redError = Colorize(error)..red(); - print(redError); - throw ToolExit(1); - } - - bool isPlatformInterface = pubspec.name.endsWith("_platform_interface"); - if (isPlatformInterface && - allowedNextVersions[headVersion] == - NextVersionType.BREAKING_MAJOR) { - final String error = '$pubspecPath breaking change detected.\n' - 'Breaking changes to platform interfaces are strongly discouraged.\n'; - final Colorize redError = Colorize(error)..red(); - print(redError); - throw ToolExit(1); - } - } on io.ProcessException { - print('Unable to find pubspec in master for $pubspecPath.' - ' Safe to ignore if the project is new.'); - } - } - - print('No version check errors found!'); - } -} diff --git a/packages/plugin_tools/lib/src/xctest_command.dart b/packages/plugin_tools/lib/src/xctest_command.dart deleted file mode 100644 index d90b7a8fbfea..000000000000 --- a/packages/plugin_tools/lib/src/xctest_command.dart +++ /dev/null @@ -1,216 +0,0 @@ -// 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 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; - -import 'common.dart'; - -const String _kiOSDestination = 'ios-destination'; -const String _kTarget = 'target'; -const String _kSkip = 'skip'; -const String _kXcodeBuildCommand = 'xcodebuild'; -const String _kXCRunCommand = 'xcrun'; -const String _kFoundNoSimulatorsMessage = - 'Cannot find any available simulators, tests failed'; - -/// The command to run iOS' XCTests in plugins, this should work for both XCUnitTest and XCUITest targets. -/// The tests target have to be added to the xcode project of the example app. Usually at "example/ios/Runner.xcodeproj". -/// The command takes a "-target" argument which has to match the target of the test target. -/// For information on how to add test target in an xcode project, see https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/UnitTesting.html -class XCTestCommand extends PluginCommand { - XCTestCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption( - _kiOSDestination, - help: - 'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n' - 'this is passed to the `-destination` argument in xcodebuild command.\n' - 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', - ); - argParser.addOption(_kTarget, - help: 'The test target.\n' - 'This is the xcode project test target. This is passed to the `-scheme` argument in the xcodebuild command. \n' - 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the scheme'); - argParser.addMultiOption(_kSkip, - help: 'Plugins to skip while running this command. \n'); - } - - @override - final String name = 'xctest'; - - @override - final String description = 'Runs the xctests in the iOS example apps.\n\n' - 'This command requires "flutter" to be in your path.'; - - @override - Future run() async { - if (argResults[_kTarget] == null) { - // TODO(cyanglaz): Automatically find all the available testing schemes if this argument is not specified. - // https://github.com/flutter/flutter/issues/68419 - print('--$_kTarget must be specified'); - throw ToolExit(1); - } - - String destination = argResults[_kiOSDestination]; - if (destination == null) { - String simulatorId = await _findAvailableIphoneSimulator(); - if (simulatorId == null) { - print(_kFoundNoSimulatorsMessage); - throw ToolExit(1); - } - destination = 'id=$simulatorId'; - } - - checkSharding(); - - final String target = argResults[_kTarget]; - final List skipped = argResults[_kSkip]; - - List failingPackages = []; - await for (Directory plugin in getPlugins()) { - // Start running for package. - final String packageName = - p.relative(plugin.path, from: packagesDir.path); - print('Start running for $packageName ...'); - if (!isIosPlugin(plugin, fileSystem)) { - print('iOS is not supported by this plugin.'); - print('\n\n'); - continue; - } - if (skipped.contains(packageName)) { - print('$packageName was skipped with the --skip flag.'); - print('\n\n'); - continue; - } - for (Directory example in getExamplesForPlugin(plugin)) { - // Look for the test scheme in the example app. - print('Look for target named: $_kTarget ...'); - final List findSchemeArgs = [ - '-project', - 'ios/Runner.xcodeproj', - '-list', - '-json' - ]; - final String completeFindSchemeCommand = - '$_kXcodeBuildCommand ${findSchemeArgs.join(' ')}'; - print(completeFindSchemeCommand); - final io.ProcessResult xcodeprojListResult = await processRunner - .run(_kXcodeBuildCommand, findSchemeArgs, workingDir: example); - if (xcodeprojListResult.exitCode != 0) { - print('Error occurred while running "$completeFindSchemeCommand":\n' - '${xcodeprojListResult.stderr}'); - failingPackages.add(packageName); - print('\n\n'); - continue; - } - - final String xcodeprojListOutput = xcodeprojListResult.stdout; - Map xcodeprojListOutputJson = - jsonDecode(xcodeprojListOutput); - if (!xcodeprojListOutputJson['project']['targets'].contains(target)) { - failingPackages.add(packageName); - print('$target not configured for $packageName, test failed.'); - print( - 'Please check the scheme for the test target if it matches the name $target.\n' - 'If this plugin does not have an XCTest target, use the $_kSkip flag in the $name command to skip the plugin.'); - print('\n\n'); - continue; - } - // Found the scheme, running tests - print('Running XCTests:$target for $packageName ...'); - final List xctestArgs = [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - target, - '-destination', - destination, - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ]; - final String completeTestCommand = - '$_kXcodeBuildCommand ${xctestArgs.join(' ')}'; - print(completeTestCommand); - final int exitCode = await processRunner - .runAndStream(_kXcodeBuildCommand, xctestArgs, workingDir: example); - if (exitCode == 0) { - print('Successfully ran xctest for $packageName'); - } else { - failingPackages.add(packageName); - } - } - } - - // Command end, print reports. - if (failingPackages.isEmpty) { - print("All XCTests have passed!"); - } else { - print( - 'The following packages are failing XCTests (see above for details):'); - for (String package in failingPackages) { - print(' * $package'); - } - throw ToolExit(1); - } - } - - Future _findAvailableIphoneSimulator() async { - // Find the first available destination if not specified. - final List findSimulatorsArguments = [ - 'simctl', - 'list', - '--json' - ]; - final String findSimulatorCompleteCommand = - '$_kXCRunCommand ${findSimulatorsArguments.join(' ')}'; - print('Looking for available simulators...'); - print(findSimulatorCompleteCommand); - final io.ProcessResult findSimulatorsResult = - await processRunner.run(_kXCRunCommand, findSimulatorsArguments); - if (findSimulatorsResult.exitCode != 0) { - print('Error occurred while running "$findSimulatorCompleteCommand":\n' - '${findSimulatorsResult.stderr}'); - throw ToolExit(1); - } - final Map simulatorListJson = - jsonDecode(findSimulatorsResult.stdout); - final List runtimes = simulatorListJson['runtimes']; - final Map devices = simulatorListJson['devices']; - if (runtimes.isEmpty || devices.isEmpty) { - return null; - } - String id; - // Looking for runtimes, trying to find one with highest OS version. - for (Map runtimeMap in runtimes.reversed) { - if (!runtimeMap['name'].contains('iOS')) { - continue; - } - final String runtimeID = runtimeMap['identifier']; - final List devicesForRuntime = devices[runtimeID]; - if (devicesForRuntime.isEmpty) { - continue; - } - // Looking for runtimes, trying to find latest version of device. - for (Map device in devicesForRuntime.reversed) { - if (device['availabilityError'] != null || - (device['isAvailable'] as bool == false)) { - continue; - } - id = device['udid']; - print('device selected: $device'); - return id; - } - } - return null; - } -} diff --git a/packages/plugin_tools/pubspec.yaml b/packages/plugin_tools/pubspec.yaml deleted file mode 100644 index f8b452910236..000000000000 --- a/packages/plugin_tools/pubspec.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: flutter_plugin_tools -description: Productivity utils for hosting multiple plugins within one repository. -homepage: https://github.com/flutter/plugin_tools -version: 0.0.45+1 - -dependencies: - args: "^1.4.3" - path: "^1.6.1" - http: "^0.12.1" - async: "^2.0.7" - yaml: "^2.1.15" - quiver: "^2.0.2" - pub_semver: ^1.4.2 - colorize: ^2.0.0 - git: ^1.0.0 - platform: ^2.2.0 - pubspec_parse: "^0.1.4" - test: ^1.6.4 - meta: ^1.1.7 - file: ^5.0.10 - uuid: ^2.0.4 - http_multi_server: ^2.2.0 - -dev_dependencies: - matcher: ^0.12.6 - mockito: ^4.1.1 - pedantic: 1.8.0 - -environment: - sdk: ">=2.3.0 <3.0.0" diff --git a/packages/plugin_tools/test/analyze_command_test.dart b/packages/plugin_tools/test/analyze_command_test.dart deleted file mode 100644 index 9e7a42bbb680..000000000000 --- a/packages/plugin_tools/test/analyze_command_test.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/analyze_command.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - RecordingProcessRunner processRunner; - CommandRunner runner; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final AnalyzeCommand analyzeCommand = AnalyzeCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner('analyze_command', 'Test for analyze_command'); - runner.addCommand(analyzeCommand); - }); - - tearDown(() { - mockPackagesDir.deleteSync(recursive: true); - }); - - test('analyzes all packages', () async { - final Directory plugin1Dir = await createFakePlugin('a'); - final Directory plugin2Dir = await createFakePlugin('b'); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - await runner.run(['analyze']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('pub', ['global', 'activate', 'tuneup'], - mockPackagesDir.path), - ProcessCall('flutter', ['packages', 'get'], plugin1Dir.path), - ProcessCall('flutter', ['packages', 'get'], plugin2Dir.path), - ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], - plugin1Dir.path), - ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], - plugin2Dir.path), - ])); - }); - - group('verifies analysis settings', () { - test('fails analysis_options.yaml', () async { - await createFakePlugin('foo', withExtraFiles: >[ - ['analysis_options.yaml'] - ]); - - await expectLater(() => runner.run(['analyze']), - throwsA(const TypeMatcher())); - }); - - test('fails .analysis_options', () async { - await createFakePlugin('foo', withExtraFiles: >[ - ['.analysis_options'] - ]); - - await expectLater(() => runner.run(['analyze']), - throwsA(const TypeMatcher())); - }); - - test('takes an allow list', () async { - final Directory pluginDir = - await createFakePlugin('foo', withExtraFiles: >[ - ['analysis_options.yaml'] - ]); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - await runner.run(['analyze', '--custom-analysis', 'foo']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('pub', ['global', 'activate', 'tuneup'], - mockPackagesDir.path), - ProcessCall('flutter', ['packages', 'get'], pluginDir.path), - ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], - pluginDir.path), - ])); - }); - }); -} diff --git a/packages/plugin_tools/test/build_examples_command_test.dart b/packages/plugin_tools/test/build_examples_command_test.dart deleted file mode 100644 index eaf5049dcc02..000000000000 --- a/packages/plugin_tools/test/build_examples_command_test.dart +++ /dev/null @@ -1,470 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/build_examples_command.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - group('test build_example_command', () { - CommandRunner runner; - RecordingProcessRunner processRunner; - final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final BuildExamplesCommand command = BuildExamplesCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner( - 'build_examples_command', 'Test for build_example_command'); - runner.addCommand(command); - cleanupPackages(); - }); - - test('building for iOS when plugin is not set up for iOS results in no-op', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--ipa', '--no-macos']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING IPA for $packageName', - 'iOS is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - - test('building for ios', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'build-examples', - '--ipa', - '--no-macos', - '--enable-experiment=exp1' - ]); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING IPA for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'build', - 'ios', - '--no-codesign', - '--enable-experiment=exp1' - ], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test( - 'building for Linux when plugin is not set up for Linux results in no-op', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--linux']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING Linux for $packageName', - 'Linux is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --linux with no - // Linux implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - - test('building for Linux', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--linux']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING Linux for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(flutterCommand, ['build', 'linux'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test('building for macos with no implementation results in no-op', - () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['example', 'test'], - ]); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--macos']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING macOS for $packageName', - '\macOS is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - test('building for macos', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ['example', 'macos', 'macos.swift'], - ], - isMacOsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--macos']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING macOS for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(flutterCommand, ['pub', 'get'], - pluginExampleDirectory.path), - ProcessCall(flutterCommand, ['build', 'macos'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test( - 'building for Windows when plugin is not set up for Windows results in no-op', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isWindowsPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--windows']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING Windows for $packageName', - 'Windows is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - - test('building for windows', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isWindowsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--windows']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING Windows for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(flutterCommand, ['build', 'windows'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test( - 'building for Android when plugin is not set up for Android results in no-op', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--apk', '--no-ipa']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING APK for $packageName', - 'Android is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - - test('building for android', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isAndroidPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'build-examples', - '--apk', - '--no-ipa', - '--no-macos', - ]); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING APK for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(flutterCommand, ['build', 'apk'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test('enable-experiment flag for Android', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isAndroidPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - await runCapturingPrint(runner, [ - 'build-examples', - '--apk', - '--no-ipa', - '--no-macos', - '--enable-experiment=exp1' - ]); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - ['build', 'apk', '--enable-experiment=exp1'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test('enable-experiment flag for ios', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - await runCapturingPrint(runner, [ - 'build-examples', - '--ipa', - '--no-macos', - '--enable-experiment=exp1' - ]); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'build', - 'ios', - '--no-codesign', - '--enable-experiment=exp1' - ], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - }); -} diff --git a/packages/plugin_tools/test/common_test.dart b/packages/plugin_tools/test/common_test.dart deleted file mode 100644 index b3504c2358d9..000000000000 --- a/packages/plugin_tools/test/common_test.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - RecordingProcessRunner processRunner; - CommandRunner runner; - List plugins; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - plugins = []; - final SamplePluginCommand samplePluginCommand = SamplePluginCommand( - plugins, - mockPackagesDir, - mockFileSystem, - processRunner: processRunner, - ); - runner = - CommandRunner('common_command', 'Test for common functionality'); - runner.addCommand(samplePluginCommand); - }); - - tearDown(() { - mockPackagesDir.deleteSync(recursive: true); - }); - - test('all plugins from file system', () async { - final Directory plugin1 = createFakePlugin('plugin1'); - final Directory plugin2 = createFakePlugin('plugin2'); - await runner.run(['sample']); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); - }); - - test('exclude plugins when plugins flag is specified', () async { - createFakePlugin('plugin1'); - final Directory plugin2 = createFakePlugin('plugin2'); - await runner.run( - ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); - expect(plugins, unorderedEquals([plugin2.path])); - }); - - test('exclude plugins when plugins flag isn\'t specified', () async { - createFakePlugin('plugin1'); - createFakePlugin('plugin2'); - await runner.run(['sample', '--exclude=plugin1,plugin2']); - expect(plugins, unorderedEquals([])); - }); - - test('exclude federated plugins when plugins flag is specified', () async { - createFakePlugin('plugin1', parentDirectoryName: 'federated'); - final Directory plugin2 = createFakePlugin('plugin2'); - await runner.run([ - 'sample', - '--plugins=federated/plugin1,plugin2', - '--exclude=federated/plugin1' - ]); - expect(plugins, unorderedEquals([plugin2.path])); - }); - - test('exclude entire federated plugins when plugins flag is specified', - () async { - createFakePlugin('plugin1', parentDirectoryName: 'federated'); - final Directory plugin2 = createFakePlugin('plugin2'); - await runner.run([ - 'sample', - '--plugins=federated/plugin1,plugin2', - '--exclude=federated' - ]); - expect(plugins, unorderedEquals([plugin2.path])); - }); -} - -class SamplePluginCommand extends PluginCommand { - SamplePluginCommand( - this.plugins_, - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner); - - List plugins_; - - @override - final String name = 'sample'; - - @override - final String description = 'sample command'; - - @override - Future run() async { - await for (Directory package in getPlugins()) { - this.plugins_.add(package.path); - } - } -} diff --git a/packages/plugin_tools/test/drive_examples_command_test.dart b/packages/plugin_tools/test/drive_examples_command_test.dart deleted file mode 100644 index f4bdd95c1664..000000000000 --- a/packages/plugin_tools/test/drive_examples_command_test.dart +++ /dev/null @@ -1,505 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - group('test drive_example_command', () { - CommandRunner runner; - RecordingProcessRunner processRunner; - final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final DriveExamplesCommand command = DriveExamplesCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner( - 'drive_examples_command', 'Test for drive_example_command'); - runner.addCommand(command); - }); - - tearDown(() { - cleanupPackages(); - }); - - test('driving under folder "test"', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test', 'plugin.dart'], - ], - isIosPlugin: true, - isAndroidPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving under folder "test_driver"', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isAndroidPlugin: true, - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving under folder "test_driver" when test files are missing"', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ], - isAndroidPlugin: true, - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - await expectLater( - () => runCapturingPrint(runner, ['drive-examples']), - throwsA(const TypeMatcher())); - }); - - test( - 'driving under folder "test_driver" when targets are under "integration_test"', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'integration_test.dart'], - ['example', 'integration_test', 'bar_test.dart'], - ['example', 'integration_test', 'foo_test.dart'], - ['example', 'integration_test', 'ignore_me.dart'], - ], - isAndroidPlugin: true, - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String driverTestPath = p.join('test_driver', 'integration_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - p.join('integration_test', 'bar_test.dart'), - ], - pluginExampleDirectory.path), - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - p.join('integration_test', 'foo_test.dart'), - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not support Linux is a no-op', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isMacOsPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--linux', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running drive-examples --linux on a non-Linux - // plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving on a Linux plugin', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isLinuxPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--linux', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '-d', - 'linux', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not suppport macOS is a no-op', () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ]); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--macos', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running drive-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, []); - }); - test('driving on a macOS plugin', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ['example', 'macos', 'macos.swift'], - ], - isMacOsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--macos', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '-d', - 'macos', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not suppport windows is a no-op', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isMacOsPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--windows', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running drive-examples --windows on a non-windows - // plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving on a Windows plugin', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isWindowsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--windows', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '-d', - 'windows', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not support mobile is no-op', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isMacOsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running drive-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('enable-experiment flag', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test', 'plugin.dart'], - ], - isIosPlugin: true, - isAndroidPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - await runCapturingPrint(runner, [ - 'drive-examples', - '--enable-experiment=exp1', - ]); - - String deviceTestPath = p.join('test', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--enable-experiment=exp1', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - }); -} diff --git a/packages/plugin_tools/test/firebase_test_lab_test.dart b/packages/plugin_tools/test/firebase_test_lab_test.dart deleted file mode 100644 index 97b977619d57..000000000000 --- a/packages/plugin_tools/test/firebase_test_lab_test.dart +++ /dev/null @@ -1,256 +0,0 @@ -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$FirebaseTestLabCommand', () { - final List printedMessages = []; - CommandRunner runner; - RecordingProcessRunner processRunner; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final FirebaseTestLabCommand command = FirebaseTestLabCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner, - print: (Object message) => printedMessages.add(message.toString())); - - runner = CommandRunner( - 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); - runner.addCommand(command); - }); - - tearDown(() { - printedMessages.clear(); - }); - - test('retries gcloud set', () async { - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(1); - processRunner.processToReturn = mockProcess; - createFakePlugin('plugin', withExtraFiles: >[ - ['lib/test/should_not_run_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e_test.dart'], - ['example', 'android', 'gradlew'], - ['example', 'should_not_run_e2e.dart'], - [ - 'example', - 'android', - 'app', - 'src', - 'androidTest', - 'MainActivityTest.java' - ], - ]); - await expectLater( - () => runCapturingPrint(runner, ['firebase-test-lab']), - throwsA(const TypeMatcher())); - expect( - printedMessages, - contains( - "\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.")); - }); - - test('runs e2e tests', () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['test', 'plugin_test.dart'], - ['test', 'plugin_e2e.dart'], - ['should_not_run_e2e.dart'], - ['lib/test/should_not_run_e2e.dart'], - ['example', 'test', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e_test.dart'], - ['example', 'integration_test', 'foo_test.dart'], - ['example', 'integration_test', 'should_not_run.dart'], - ['example', 'android', 'gradlew'], - ['example', 'should_not_run_e2e.dart'], - [ - 'example', - 'android', - 'app', - 'src', - 'androidTest', - 'MainActivityTest.java' - ], - ]); - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=flame,version=29', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - ]); - - expect( - printedMessages, - orderedEquals([ - '\nRUNNING FIREBASE TEST LAB TESTS for plugin', - '\nFirebase project configured.', - '\n\n', - 'All Firebase Test Lab tests successful!', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true'.split(' '), - '/packages/plugin/example/android'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ]), - ); - }); - - test('experimental flag', () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['test', 'plugin_test.dart'], - ['test', 'plugin_e2e.dart'], - ['should_not_run_e2e.dart'], - ['lib/test/should_not_run_e2e.dart'], - ['example', 'test', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e_test.dart'], - ['example', 'integration_test', 'foo_test.dart'], - ['example', 'integration_test', 'should_not_run.dart'], - ['example', 'android', 'gradlew'], - ['example', 'should_not_run_e2e.dart'], - [ - 'example', - 'android', - 'app', - 'src', - 'androidTest', - 'MainActivityTest.java' - ], - ]); - - await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=flame,version=29', - '--test-run-id', - 'testRunId', - '--enable-experiment=exp1', - ]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ]), - ); - - cleanupPackages(); - }); - }); -} diff --git a/packages/plugin_tools/test/lint_podspecs_command_test.dart b/packages/plugin_tools/test/lint_podspecs_command_test.dart deleted file mode 100644 index 49d6ad4d8e20..000000000000 --- a/packages/plugin_tools/test/lint_podspecs_command_test.dart +++ /dev/null @@ -1,202 +0,0 @@ -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$LintPodspecsCommand', () { - CommandRunner runner; - MockPlatform mockPlatform; - final RecordingProcessRunner processRunner = RecordingProcessRunner(); - List printedMessages; - - setUp(() { - initializeFakePackages(); - - printedMessages = []; - mockPlatform = MockPlatform(); - when(mockPlatform.isMacOS).thenReturn(true); - final LintPodspecsCommand command = LintPodspecsCommand( - mockPackagesDir, - mockFileSystem, - processRunner: processRunner, - platform: mockPlatform, - print: (Object message) => printedMessages.add(message.toString()), - ); - - runner = - CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); - runner.addCommand(command); - final MockProcess mockLintProcess = MockProcess(); - mockLintProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockLintProcess; - processRunner.recordedCalls.clear(); - }); - - tearDown(() { - cleanupPackages(); - }); - - test('only runs on macOS', () async { - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'], - ]); - - when(mockPlatform.isMacOS).thenReturn(false); - await runner.run(['podspecs']); - - expect( - processRunner.recordedCalls, - equals([]), - ); - }); - - test('runs pod lib lint on a podspec', () async { - Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['ios', 'plugin1.podspec'], - ['bogus.dart'], // Ignore non-podspecs. - ]); - - processRunner.resultStdout = 'Foo'; - processRunner.resultStderr = 'Bar'; - - await runner.run(['podspecs']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), - '--analyze', - '--use-libraries' - ], - mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), - '--analyze', - ], - mockPackagesDir.path), - ]), - ); - - expect( - printedMessages, contains('Linting and analyzing plugin1.podspec')); - expect(printedMessages, contains('Foo')); - expect(printedMessages, contains('Bar')); - }); - - test('skips podspecs with known issues', () async { - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'] - ]); - createFakePlugin('plugin2', withExtraFiles: >[ - ['plugin2.podspec'] - ]); - - await runner - .run(['podspecs', '--skip=plugin1', '--skip=plugin2']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), - ]), - ); - }); - - test('skips analyzer for podspecs with known warnings', () async { - Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'], - ]); - - await runner.run(['podspecs', '--no-analyze=plugin1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - '--use-libraries' - ], - mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - ], - mockPackagesDir.path), - ]), - ); - - expect(printedMessages, contains('Linting plugin1.podspec')); - }); - - test('allow warnings for podspecs with known warnings', () async { - Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'], - ]); - - await runner.run(['podspecs', '--ignore-warnings=plugin1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - '--allow-warnings', - '--analyze', - '--use-libraries' - ], - mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - '--allow-warnings', - '--analyze', - ], - mockPackagesDir.path), - ]), - ); - - expect( - printedMessages, contains('Linting and analyzing plugin1.podspec')); - }); - }); -} - -class MockPlatform extends Mock implements Platform {} diff --git a/packages/plugin_tools/test/list_command_test.dart b/packages/plugin_tools/test/list_command_test.dart deleted file mode 100644 index 478625283dd0..000000000000 --- a/packages/plugin_tools/test/list_command_test.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/list_command.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - group('$ListCommand', () { - CommandRunner runner; - - setUp(() { - initializeFakePackages(); - final ListCommand command = ListCommand(mockPackagesDir, mockFileSystem); - - runner = CommandRunner('list_test', 'Test for $ListCommand'); - runner.addCommand(command); - }); - - test('lists plugins', () async { - createFakePlugin('plugin1'); - createFakePlugin('plugin2'); - - final List plugins = - await runCapturingPrint(runner, ['list', '--type=plugin']); - - expect( - plugins, - orderedEquals([ - '/packages/plugin1', - '/packages/plugin2', - ]), - ); - - cleanupPackages(); - }); - - test('lists examples', () async { - createFakePlugin('plugin1', withSingleExample: true); - createFakePlugin('plugin2', - withExamples: ['example1', 'example2']); - createFakePlugin('plugin3'); - - final List examples = - await runCapturingPrint(runner, ['list', '--type=example']); - - expect( - examples, - orderedEquals([ - '/packages/plugin1/example', - '/packages/plugin2/example/example1', - '/packages/plugin2/example/example2', - ]), - ); - - cleanupPackages(); - }); - - test('lists packages', () async { - createFakePlugin('plugin1', withSingleExample: true); - createFakePlugin('plugin2', - withExamples: ['example1', 'example2']); - createFakePlugin('plugin3'); - - final List packages = - await runCapturingPrint(runner, ['list', '--type=package']); - - expect( - packages, - unorderedEquals([ - '/packages/plugin1', - '/packages/plugin1/example', - '/packages/plugin2', - '/packages/plugin2/example/example1', - '/packages/plugin2/example/example2', - '/packages/plugin3', - ]), - ); - - cleanupPackages(); - }); - - test('lists files', () async { - createFakePlugin('plugin1', withSingleExample: true); - createFakePlugin('plugin2', - withExamples: ['example1', 'example2']); - createFakePlugin('plugin3'); - - final List examples = - await runCapturingPrint(runner, ['list', '--type=file']); - - expect( - examples, - unorderedEquals([ - '/packages/plugin1/pubspec.yaml', - '/packages/plugin1/example/pubspec.yaml', - '/packages/plugin2/pubspec.yaml', - '/packages/plugin2/example/example1/pubspec.yaml', - '/packages/plugin2/example/example2/pubspec.yaml', - '/packages/plugin3/pubspec.yaml', - ]), - ); - - cleanupPackages(); - }); - - test('lists plugins using federated plugin layout', () async { - createFakePlugin('plugin1'); - - // Create a federated plugin by creating a directory under the packages - // directory with several packages underneath. - final Directory federatedPlugin = - mockPackagesDir.childDirectory('my_plugin')..createSync(); - final Directory clientLibrary = - federatedPlugin.childDirectory('my_plugin')..createSync(); - createFakePubspec(clientLibrary); - final Directory webLibrary = - federatedPlugin.childDirectory('my_plugin_web')..createSync(); - createFakePubspec(webLibrary); - final Directory macLibrary = - federatedPlugin.childDirectory('my_plugin_macos')..createSync(); - createFakePubspec(macLibrary); - - // Test without specifying `--type`. - final List plugins = - await runCapturingPrint(runner, ['list']); - - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - '/packages/my_plugin/my_plugin', - '/packages/my_plugin/my_plugin_web', - '/packages/my_plugin/my_plugin_macos', - ]), - ); - - cleanupPackages(); - }); - - test('can filter plugins with the --plugins argument', () async { - createFakePlugin('plugin1'); - - // Create a federated plugin by creating a directory under the packages - // directory with several packages underneath. - final Directory federatedPlugin = - mockPackagesDir.childDirectory('my_plugin')..createSync(); - final Directory clientLibrary = - federatedPlugin.childDirectory('my_plugin')..createSync(); - createFakePubspec(clientLibrary); - final Directory webLibrary = - federatedPlugin.childDirectory('my_plugin_web')..createSync(); - createFakePubspec(webLibrary); - final Directory macLibrary = - federatedPlugin.childDirectory('my_plugin_macos')..createSync(); - createFakePubspec(macLibrary); - - List plugins = await runCapturingPrint( - runner, ['list', '--plugins=plugin1']); - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - ]), - ); - - plugins = await runCapturingPrint( - runner, ['list', '--plugins=my_plugin']); - expect( - plugins, - unorderedEquals([ - '/packages/my_plugin/my_plugin', - '/packages/my_plugin/my_plugin_web', - '/packages/my_plugin/my_plugin_macos', - ]), - ); - - plugins = await runCapturingPrint( - runner, ['list', '--plugins=my_plugin/my_plugin_web']); - expect( - plugins, - unorderedEquals([ - '/packages/my_plugin/my_plugin_web', - ]), - ); - - plugins = await runCapturingPrint(runner, - ['list', '--plugins=my_plugin/my_plugin_web,plugin1']); - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - '/packages/my_plugin/my_plugin_web', - ]), - ); - }); - }); -} diff --git a/packages/plugin_tools/test/mocks.dart b/packages/plugin_tools/test/mocks.dart deleted file mode 100644 index 3e17ff8efd32..000000000000 --- a/packages/plugin_tools/test/mocks.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:async'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:mockito/mockito.dart'; - -class MockProcess extends Mock implements io.Process { - final Completer exitCodeCompleter = Completer(); - final StreamController> stdoutController = - StreamController>(); - final StreamController> stderrController = - StreamController>(); - final MockIOSink stdinMock = MockIOSink(); - - @override - Future get exitCode => exitCodeCompleter.future; - - @override - Stream> get stdout => stdoutController.stream; - - @override - Stream> get stderr => stderrController.stream; - - @override - IOSink get stdin => stdinMock; -} - -class MockIOSink extends Mock implements IOSink { - List lines = []; - - @override - void writeln([Object obj = ""]) => lines.add(obj); -} diff --git a/packages/plugin_tools/test/publish_plugin_command_test.dart b/packages/plugin_tools/test/publish_plugin_command_test.dart deleted file mode 100644 index d01bfa67ce24..000000000000 --- a/packages/plugin_tools/test/publish_plugin_command_test.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:git/git.dart'; -import 'package:matcher/matcher.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - const String testPluginName = 'foo'; - final List printedMessages = []; - - Directory parentDir; - Directory pluginDir; - GitDir gitDir; - TestProcessRunner processRunner; - CommandRunner commandRunner; - MockStdin mockStdin; - - setUp(() async { - // This test uses a local file system instead of an in memory one throughout - // so that git actually works. In setup we initialize a mono repo of plugins - // with one package and commit everything to Git. - parentDir = const LocalFileSystem() - .systemTempDirectory - .createTempSync('publish_plugin_command_test-'); - initializeFakePackages(parentDir: parentDir); - pluginDir = createFakePlugin(testPluginName, withSingleExample: false); - assert(pluginDir != null && pluginDir.existsSync()); - createFakePubspec(pluginDir, includeVersion: true); - io.Process.runSync('git', ['init'], - workingDirectory: mockPackagesDir.path); - gitDir = await GitDir.fromExisting(mockPackagesDir.path); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Initial commit']); - processRunner = TestProcessRunner(); - mockStdin = MockStdin(); - commandRunner = CommandRunner('tester', '') - ..addCommand(PublishPluginCommand( - mockPackagesDir, const LocalFileSystem(), - processRunner: processRunner, - print: (Object message) => printedMessages.add(message.toString()), - stdinput: mockStdin)); - }); - - tearDown(() { - parentDir.deleteSync(recursive: true); - printedMessages.clear(); - }); - - group('Initial validation', () { - test('requires a package flag', () async { - await expectLater(() => commandRunner.run(['publish-plugin']), - throwsA(const TypeMatcher())); - - expect( - printedMessages.last, contains("Must specify a package to publish.")); - }); - - test('requires an existing flag', () async { - await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', 'iamerror']), - throwsA(const TypeMatcher())); - - expect(printedMessages.last, contains('iamerror does not exist')); - }); - - test('refuses to proceed with dirty files', () async { - pluginDir.childFile('tmp').createSync(); - - await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', testPluginName]), - throwsA(const TypeMatcher())); - - expect( - printedMessages.last, - contains( - "There are files in the package directory that haven't been saved in git.")); - }); - - test('fails immediately if the remote doesn\'t exist', () async { - await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', testPluginName]), - throwsA(const TypeMatcher())); - - expect(processRunner.results.last.stderr, contains("No such remote")); - }); - - test("doesn't validate the remote if it's not pushing tags", () async { - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release' - ]); - - expect(printedMessages.last, 'Done!'); - }); - }); - - group('Publishes package', () { - test('while showing all output from pub publish to the user', () async { - final Future publishCommand = commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release' - ]); - processRunner.mockPublishProcess.stdoutController.add(utf8.encode('Foo')); - processRunner.mockPublishProcess.stderrController.add(utf8.encode('Bar')); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - - await publishCommand; - - expect(printedMessages, contains('Foo')); - expect(printedMessages, contains('Bar')); - }); - - test('forwards input from the user to `pub publish`', () async { - final Future publishCommand = commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release' - ]); - mockStdin.controller.add(utf8.encode('user input')); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - - await publishCommand; - - expect(processRunner.mockPublishProcess.stdinMock.lines, - contains('user input')); - }); - - test('forwards --pub-publish-flags to pub publish', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release', - '--pub-publish-flags', - '--dry-run,--server=foo' - ]); - - expect(processRunner.mockPublishArgs.length, 4); - expect(processRunner.mockPublishArgs[0], 'pub'); - expect(processRunner.mockPublishArgs[1], 'publish'); - expect(processRunner.mockPublishArgs[2], '--dry-run'); - expect(processRunner.mockPublishArgs[3], '--server=foo'); - }); - - test('throws if pub publish fails', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(128); - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release', - ]), - throwsA(const TypeMatcher())); - - expect(printedMessages, contains("Publish failed. Exiting.")); - }); - }); - - group('Tags release', () { - test('with the version and name from the pubspec.yaml', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - ]); - - final String tag = - (await gitDir.runCommand(['show-ref', 'fake_package-v0.0.1'])) - .stdout; - expect(tag, isNotEmpty); - }); - - test('only if publishing succeeded', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(128); - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - ]), - throwsA(const TypeMatcher())); - - expect(printedMessages, contains("Publish failed. Exiting.")); - final String tag = (await gitDir.runCommand( - ['show-ref', 'fake_package-v0.0.1'], - throwOnError: false)) - .stdout; - expect(tag, isEmpty); - }); - }); - - group('Pushes tags', () { - setUp(() async { - await gitDir.runCommand( - ['remote', 'add', 'upstream', 'http://localhost:8000']); - }); - - test('requires user confirmation', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - mockStdin.readLineOutput = 'help'; - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - ]), - throwsA(const TypeMatcher())); - - expect(printedMessages, contains('Tag push canceled.')); - }); - - test('to upstream by default', () async { - await gitDir.runCommand(['tag', 'garbage']); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - mockStdin.readLineOutput = 'y'; - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - ]); - - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); - expect(printedMessages.last, 'Done!'); - }); - - test('to different remotes based on a flag', () async { - await gitDir.runCommand( - ['remote', 'add', 'origin', 'http://localhost:8001']); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - mockStdin.readLineOutput = 'y'; - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--remote', - 'origin', - ]); - - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'origin'); - expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); - expect(printedMessages.last, 'Done!'); - }); - - test('only if tagging and pushing to remotes are both enabled', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-tag-release', - ]); - - expect(processRunner.pushTagsArgs.isEmpty, isTrue); - expect(printedMessages.last, 'Done!'); - }); - }); -} - -class TestProcessRunner extends ProcessRunner { - final List results = []; - final MockProcess mockPublishProcess = MockProcess(); - final List mockPublishArgs = []; - final MockProcessResult mockPushTagsResult = MockProcessResult(); - final List pushTagsArgs = []; - - @override - Future runAndExitOnError( - String executable, - List args, { - Directory workingDir, - }) async { - // Don't ever really push tags. - if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { - pushTagsArgs.addAll(args); - return mockPushTagsResult; - } - - final io.ProcessResult result = io.Process.runSync(executable, args, - workingDirectory: workingDir?.path); - results.add(result); - if (result.exitCode != 0) { - throw ToolExit(result.exitCode); - } - return result; - } - - @override - Future start(String executable, List args, - {Directory workingDirectory}) async { - /// Never actually publish anything. Start is always and only used for this - /// since it returns something we can route stdin through. - assert(executable == 'flutter' && - args.isNotEmpty && - args[0] == 'pub' && - args[1] == 'publish'); - mockPublishArgs.addAll(args); - return mockPublishProcess; - } -} - -class MockStdin extends Mock implements io.Stdin { - final StreamController> controller = StreamController>(); - String readLineOutput; - - @override - Stream transform(StreamTransformer streamTransformer) { - return controller.stream.transform(streamTransformer); - } - - @override - StreamSubscription> listen(void onData(List event), - {Function onError, void onDone(), bool cancelOnError}) { - return controller.stream.listen(onData, - onError: onError, onDone: onDone, cancelOnError: cancelOnError); - } - - @override - String readLineSync( - {Encoding encoding = io.systemEncoding, - bool retainNewlines = false}) => - readLineOutput; -} - -class MockProcessResult extends Mock implements io.ProcessResult {} diff --git a/packages/plugin_tools/test/test_command_test.dart b/packages/plugin_tools/test/test_command_test.dart deleted file mode 100644 index 514e4c27190a..000000000000 --- a/packages/plugin_tools/test/test_command_test.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/test_command.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - group('$TestCommand', () { - CommandRunner runner; - final RecordingProcessRunner processRunner = RecordingProcessRunner(); - - setUp(() { - initializeFakePackages(); - final TestCommand command = TestCommand(mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner('test_test', 'Test for $TestCommand'); - runner.addCommand(command); - }); - - tearDown(() { - cleanupPackages(); - processRunner.recordedCalls.clear(); - }); - - test('runs flutter test on each plugin', () async { - final Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = - createFakePlugin('plugin2', withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - - await runner.run(['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', ['test', '--color'], plugin1Dir.path), - ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), - ]), - ); - - cleanupPackages(); - }); - - test('skips testing plugins without test directory', () async { - createFakePlugin('plugin1'); - final Directory plugin2Dir = - createFakePlugin('plugin2', withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - - await runner.run(['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), - ]), - ); - - cleanupPackages(); - }); - - test('runs pub run test on non-Flutter packages', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', - isFlutter: true, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = createFakePlugin('plugin2', - isFlutter: false, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - - await runner.run(['test', '--enable-experiment=exp1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - ['test', '--color', '--enable-experiment=exp1'], - plugin1Dir.path), - ProcessCall('pub', ['get'], plugin2Dir.path), - ProcessCall( - 'pub', - ['run', '--enable-experiment=exp1', 'test'], - plugin2Dir.path), - ]), - ); - - cleanupPackages(); - }); - - test('runs on Chrome for web plugins', () async { - final Directory pluginDir = createFakePlugin( - 'plugin', - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ], - isFlutter: true, - isWebPlugin: true, - ); - - await runner.run(['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', - ['test', '--color', '--platform=chrome'], pluginDir.path), - ]), - ); - }); - - test('enable-experiment flag', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', - isFlutter: true, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = createFakePlugin('plugin2', - isFlutter: false, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - - await runner.run(['test', '--enable-experiment=exp1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - ['test', '--color', '--enable-experiment=exp1'], - plugin1Dir.path), - ProcessCall('pub', ['get'], plugin2Dir.path), - ProcessCall( - 'pub', - ['run', '--enable-experiment=exp1', 'test'], - plugin2Dir.path), - ]), - ); - - cleanupPackages(); - }); - }); -} diff --git a/packages/plugin_tools/test/util.dart b/packages/plugin_tools/test/util.dart deleted file mode 100644 index ec0000d13f34..000000000000 --- a/packages/plugin_tools/test/util.dart +++ /dev/null @@ -1,291 +0,0 @@ -import 'dart:async'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:platform/platform.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:quiver/collection.dart'; - -FileSystem mockFileSystem = MemoryFileSystem( - style: LocalPlatform().isWindows - ? FileSystemStyle.windows - : FileSystemStyle.posix); -Directory mockPackagesDir; - -/// Creates a mock packages directory in the mock file system. -/// -/// If [parentDir] is set the mock packages dir will be creates as a child of -/// it. If not [mockFileSystem] will be used instead. -void initializeFakePackages({Directory parentDir}) { - mockPackagesDir = - (parentDir ?? mockFileSystem.currentDirectory).childDirectory('packages'); - mockPackagesDir.createSync(); -} - -/// Creates a plugin package with the given [name] in [mockPackagesDir]. -Directory createFakePlugin( - String name, { - bool withSingleExample = false, - List withExamples = const [], - List> withExtraFiles = const >[], - bool isFlutter = true, - bool isAndroidPlugin = false, - bool isIosPlugin = false, - bool isWebPlugin = false, - bool isLinuxPlugin = false, - bool isMacOsPlugin = false, - bool isWindowsPlugin = false, - String parentDirectoryName = '', -}) { - assert(!(withSingleExample && withExamples.isNotEmpty), - 'cannot pass withSingleExample and withExamples simultaneously'); - - final Directory pluginDirectory = (parentDirectoryName != '') - ? mockPackagesDir.childDirectory(parentDirectoryName).childDirectory(name) - : mockPackagesDir.childDirectory(name); - pluginDirectory.createSync(recursive: true); - - createFakePubspec( - pluginDirectory, - name: name, - isFlutter: isFlutter, - isAndroidPlugin: isAndroidPlugin, - isIosPlugin: isIosPlugin, - isWebPlugin: isWebPlugin, - isLinuxPlugin: isLinuxPlugin, - isMacOsPlugin: isMacOsPlugin, - isWindowsPlugin: isWindowsPlugin, - ); - - if (withSingleExample) { - final Directory exampleDir = pluginDirectory.childDirectory('example') - ..createSync(); - createFakePubspec(exampleDir, - name: "${name}_example", isFlutter: isFlutter); - } else if (withExamples.isNotEmpty) { - final Directory exampleDir = pluginDirectory.childDirectory('example') - ..createSync(); - for (String example in withExamples) { - final Directory currentExample = exampleDir.childDirectory(example) - ..createSync(); - createFakePubspec(currentExample, name: example, isFlutter: isFlutter); - } - } - - for (List file in withExtraFiles) { - final List newFilePath = [pluginDirectory.path] - ..addAll(file); - final File newFile = - mockFileSystem.file(mockFileSystem.path.joinAll(newFilePath)); - newFile.createSync(recursive: true); - } - - return pluginDirectory; -} - -/// Creates a `pubspec.yaml` file with a flutter dependency. -void createFakePubspec( - Directory parent, { - String name = 'fake_package', - bool isFlutter = true, - bool includeVersion = false, - bool isAndroidPlugin = false, - bool isIosPlugin = false, - bool isWebPlugin = false, - bool isLinuxPlugin = false, - bool isMacOsPlugin = false, - bool isWindowsPlugin = false, -}) { - parent.childFile('pubspec.yaml').createSync(); - String yaml = ''' -name: $name -flutter: - plugin: - platforms: -'''; - if (isAndroidPlugin) { - yaml += ''' - android: - package: io.flutter.plugins.fake - pluginClass: FakePlugin -'''; - } - if (isIosPlugin) { - yaml += ''' - ios: - pluginClass: FLTFakePlugin -'''; - } - if (isWebPlugin) { - yaml += ''' - web: - pluginClass: FakePlugin - fileName: ${name}_web.dart -'''; - } - if (isLinuxPlugin) { - yaml += ''' - linux: - pluginClass: FakePlugin -'''; - } - if (isMacOsPlugin) { - yaml += ''' - macos: - pluginClass: FakePlugin -'''; - } - if (isWindowsPlugin) { - yaml += ''' - windows: - pluginClass: FakePlugin -'''; - } - if (isFlutter) { - yaml += ''' -dependencies: - flutter: - sdk: flutter -'''; - } - if (includeVersion) { - yaml += ''' -version: 0.0.1 -publish_to: none # Hardcoded safeguard to prevent this from somehow being published by a broken test. -'''; - } - parent.childFile('pubspec.yaml').writeAsStringSync(yaml); -} - -/// Cleans up the mock packages directory, making it an empty directory again. -void cleanupPackages() { - mockPackagesDir.listSync().forEach((FileSystemEntity entity) { - entity.deleteSync(recursive: true); - }); -} - -/// Run the command [runner] with the given [args] and return -/// what was printed. -Future> runCapturingPrint( - CommandRunner runner, List args) async { - final List prints = []; - final ZoneSpecification spec = ZoneSpecification( - print: (_, __, ___, String message) { - prints.add(message); - }, - ); - await Zone.current - .fork(specification: spec) - .run>(() => runner.run(args)); - - return prints; -} - -/// A mock [ProcessRunner] which records process calls. -class RecordingProcessRunner extends ProcessRunner { - io.Process processToReturn; - final List recordedCalls = []; - - /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. - String resultStdout; - - /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. - String resultStderr; - - @override - Future runAndStream( - String executable, - List args, { - Directory workingDir, - bool exitOnError = false, - }) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - return Future.value( - processToReturn == null ? 0 : await processToReturn.exitCode); - } - - /// Returns [io.ProcessResult] created from [processToReturn], [resultStdout], and [resultStderr]. - @override - Future run(String executable, List args, - {Directory workingDir, - bool exitOnError = false, - stdoutEncoding = io.systemEncoding, - stderrEncoding = io.systemEncoding}) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - io.ProcessResult result; - - if (processToReturn != null) { - result = io.ProcessResult( - processToReturn.pid, - await processToReturn.exitCode, - resultStdout ?? processToReturn.stdout, - resultStderr ?? processToReturn.stderr); - } - return Future.value(result); - } - - @override - Future runAndExitOnError( - String executable, - List args, { - Directory workingDir, - }) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - io.ProcessResult result; - if (processToReturn != null) { - result = io.ProcessResult( - processToReturn.pid, - await processToReturn.exitCode, - resultStdout ?? processToReturn.stdout, - resultStderr ?? processToReturn.stderr); - } - return Future.value(result); - } - - @override - Future start(String executable, List args, - {Directory workingDirectory}) async { - recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); - return Future.value(processToReturn); - } -} - -/// A recorded process call. -class ProcessCall { - const ProcessCall(this.executable, this.args, this.workingDir); - - /// The executable that was called. - final String executable; - - /// The arguments passed to [executable] in the call. - final List args; - - /// The working directory this process was called from. - final String workingDir; - - @override - bool operator ==(dynamic other) { - if (other is! ProcessCall) { - return false; - } - final ProcessCall otherCall = other; - return executable == otherCall.executable && - listsEqual(args, otherCall.args) && - workingDir == otherCall.workingDir; - } - - @override - int get hashCode => - executable?.hashCode ?? - 0 ^ args?.hashCode ?? - 0 ^ workingDir?.hashCode ?? - 0; - - @override - String toString() { - final List command = [executable]..addAll(args); - return '"${command.join(' ')}" in $workingDir'; - } -} diff --git a/packages/plugin_tools/test/version_check_test.dart b/packages/plugin_tools/test/version_check_test.dart deleted file mode 100644 index b9ace3811bff..000000000000 --- a/packages/plugin_tools/test/version_check_test.dart +++ /dev/null @@ -1,319 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:git/git.dart'; -import 'package:mockito/mockito.dart'; -import "package:test/test.dart"; -import "package:flutter_plugin_tools/src/version_check_command.dart"; -import 'package:pub_semver/pub_semver.dart'; -import 'util.dart'; - -void testAllowedVersion( - String masterVersion, - String headVersion, { - bool allowed = true, - NextVersionType nextVersionType, -}) { - final Version master = Version.parse(masterVersion); - final Version head = Version.parse(headVersion); - final Map allowedVersions = - getAllowedNextVersions(master, head); - if (allowed) { - expect(allowedVersions, contains(head)); - if (nextVersionType != null) { - expect(allowedVersions[head], equals(nextVersionType)); - } - } else { - expect(allowedVersions, isNot(contains(head))); - } -} - -class MockGitDir extends Mock implements GitDir {} - -class MockProcessResult extends Mock implements ProcessResult {} - -void main() { - group('$VersionCheckCommand', () { - CommandRunner runner; - RecordingProcessRunner processRunner; - List> gitDirCommands; - String gitDiffResponse; - Map gitShowResponses; - - setUp(() { - gitDirCommands = >[]; - gitDiffResponse = ''; - gitShowResponses = {}; - final MockGitDir gitDir = MockGitDir(); - when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0]); - final MockProcessResult mockProcessResult = MockProcessResult(); - if (invocation.positionalArguments[0][0] == 'diff') { - when(mockProcessResult.stdout).thenReturn(gitDiffResponse); - } else if (invocation.positionalArguments[0][0] == 'show') { - final String response = - gitShowResponses[invocation.positionalArguments[0][1]]; - when(mockProcessResult.stdout).thenReturn(response); - } - return Future.value(mockProcessResult); - }); - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final VersionCheckCommand command = VersionCheckCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner, gitDir: gitDir); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); - }); - - tearDown(() { - cleanupPackages(); - }); - - test('allows valid version', () async { - createFakePlugin('plugin'); - gitDiffResponse = "packages/plugin/pubspec.yaml"; - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', - }; - final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - - expect( - output, - orderedEquals([ - 'No version check errors found!', - ]), - ); - expect(gitDirCommands.length, equals(3)); - expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect(gitDirCommands[1].join(' '), - equals('show master:packages/plugin/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin/pubspec.yaml')); - }); - - test('denies invalid version', () async { - createFakePlugin('plugin'); - gitDiffResponse = "packages/plugin/pubspec.yaml"; - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', - }; - final Future> result = runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - - await expectLater( - result, - throwsA(const TypeMatcher()), - ); - expect(gitDirCommands.length, equals(3)); - expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect(gitDirCommands[1].join(' '), - equals('show master:packages/plugin/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin/pubspec.yaml')); - }); - - test('gracefully handles missing pubspec.yaml', () async { - createFakePlugin('plugin'); - gitDiffResponse = "packages/plugin/pubspec.yaml"; - mockFileSystem.currentDirectory - .childDirectory('packages') - .childDirectory('plugin') - .childFile('pubspec.yaml') - .deleteSync(); - final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - - expect( - output, - orderedEquals([ - 'No version check errors found!', - ]), - ); - expect(gitDirCommands.length, equals(1)); - expect(gitDirCommands.first.join(' '), - equals('diff --name-only master HEAD')); - }); - - test('allows minor changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface'); - gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; - gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.0.0', - 'HEAD:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.1.0', - }; - final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - expect( - output, - orderedEquals([ - 'No version check errors found!', - ]), - ); - expect(gitDirCommands.length, equals(3)); - expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect( - gitDirCommands[1].join(' '), - equals( - 'show master:packages/plugin_platform_interface/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); - }); - - test('disallows breaking changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface'); - gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; - gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.0.0', - 'HEAD:packages/plugin_platform_interface/pubspec.yaml': - 'version: 2.0.0', - }; - final Future> output = runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - await expectLater( - output, - throwsA(const TypeMatcher()), - ); - expect(gitDirCommands.length, equals(3)); - expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect( - gitDirCommands[1].join(' '), - equals( - 'show master:packages/plugin_platform_interface/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); - }); - }); - - group("Pre 1.0", () { - test("nextVersion allows patch version", () { - testAllowedVersion("0.12.0", "0.12.0+1", - nextVersionType: NextVersionType.PATCH); - testAllowedVersion("0.12.0+4", "0.12.0+5", - nextVersionType: NextVersionType.PATCH); - }); - - test("nextVersion does not allow jumping patch", () { - testAllowedVersion("0.12.0", "0.12.0+2", allowed: false); - testAllowedVersion("0.12.0+2", "0.12.0+4", allowed: false); - }); - - test("nextVersion does not allow going back", () { - testAllowedVersion("0.12.0", "0.11.0", allowed: false); - testAllowedVersion("0.12.0+2", "0.12.0+1", allowed: false); - testAllowedVersion("0.12.0+1", "0.12.0", allowed: false); - }); - - test("nextVersion allows minor version", () { - testAllowedVersion("0.12.0", "0.12.1", - nextVersionType: NextVersionType.MINOR); - testAllowedVersion("0.12.0+4", "0.12.1", - nextVersionType: NextVersionType.MINOR); - }); - - test("nextVersion does not allow jumping minor", () { - testAllowedVersion("0.12.0", "0.12.2", allowed: false); - testAllowedVersion("0.12.0+2", "0.12.3", allowed: false); - }); - }); - - group("Releasing 1.0", () { - test("nextVersion allows releasing 1.0", () { - testAllowedVersion("0.12.0", "1.0.0", - nextVersionType: NextVersionType.BREAKING_MAJOR); - testAllowedVersion("0.12.0+4", "1.0.0", - nextVersionType: NextVersionType.BREAKING_MAJOR); - }); - - test("nextVersion does not allow jumping major", () { - testAllowedVersion("0.12.0", "2.0.0", allowed: false); - testAllowedVersion("0.12.0+4", "2.0.0", allowed: false); - }); - - test("nextVersion does not allow un-releasing", () { - testAllowedVersion("1.0.0", "0.12.0+4", allowed: false); - testAllowedVersion("1.0.0", "0.12.0", allowed: false); - }); - }); - - group("Post 1.0", () { - test("nextVersion allows patch jumps", () { - testAllowedVersion("1.0.1", "1.0.2", - nextVersionType: NextVersionType.PATCH); - testAllowedVersion("1.0.0", "1.0.1", - nextVersionType: NextVersionType.PATCH); - }); - - test("nextVersion does not allow build jumps", () { - testAllowedVersion("1.0.1", "1.0.1+1", allowed: false); - testAllowedVersion("1.0.0+5", "1.0.0+6", allowed: false); - }); - - test("nextVersion does not allow skipping patches", () { - testAllowedVersion("1.0.1", "1.0.3", allowed: false); - testAllowedVersion("1.0.0", "1.0.6", allowed: false); - }); - - test("nextVersion allows minor version jumps", () { - testAllowedVersion("1.0.1", "1.1.0", - nextVersionType: NextVersionType.MINOR); - testAllowedVersion("1.0.0", "1.1.0", - nextVersionType: NextVersionType.MINOR); - }); - - test("nextVersion does not allow skipping minor versions", () { - testAllowedVersion("1.0.1", "1.2.0", allowed: false); - testAllowedVersion("1.1.0", "1.3.0", allowed: false); - }); - - test("nextVersion allows breaking changes", () { - testAllowedVersion("1.0.1", "2.0.0", - nextVersionType: NextVersionType.BREAKING_MAJOR); - testAllowedVersion("1.0.0", "2.0.0", - nextVersionType: NextVersionType.BREAKING_MAJOR); - }); - - test("nextVersion allows null safety pre prelease", () { - testAllowedVersion("1.0.1", "2.0.0-nullsafety", - nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0", "2.0.0-nullsafety", - nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0-nullsafety", "1.0.0-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0-nullsafety.1", "1.0.0-nullsafety.2", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0", "0.2.0-nullsafety", - nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0-nullsafety", "0.1.0-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0-nullsafety.1", "0.1.0-nullsafety.2", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0", "1.1.0-nullsafety", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.1.0-nullsafety", "1.1.0-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0", "0.1.1-nullsafety", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.1-nullsafety", "0.1.1-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - }); - - test("nextVersion does not allow skipping major versions", () { - testAllowedVersion("1.0.1", "3.0.0", allowed: false); - testAllowedVersion("1.1.0", "2.3.0", allowed: false); - }); - }); -} diff --git a/packages/plugin_tools/test/xctest_command_test.dart b/packages/plugin_tools/test/xctest_command_test.dart deleted file mode 100644 index 007c2e12188c..000000000000 --- a/packages/plugin_tools/test/xctest_command_test.dart +++ /dev/null @@ -1,358 +0,0 @@ -// 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 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/xctest_command.dart'; -import 'package:test/test.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -final _kDeviceListMap = { - "runtimes": [ - { - "bundlePath": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime", - "buildversion": "17A577", - "runtimeRoot": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot", - "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-0", - "version": "13.0", - "isAvailable": true, - "name": "iOS 13.0" - }, - { - "bundlePath": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime", - "buildversion": "17L255", - "runtimeRoot": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot", - "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-4", - "version": "13.4", - "isAvailable": true, - "name": "iOS 13.4" - }, - { - "bundlePath": - "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime", - "buildversion": "17T531", - "runtimeRoot": - "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot", - "identifier": "com.apple.CoreSimulator.SimRuntime.watchOS-6-2", - "version": "6.2.1", - "isAvailable": true, - "name": "watchOS 6.2" - } - ], - "devices": { - "com.apple.CoreSimulator.SimRuntime.iOS-13-4": [ - { - "dataPath": - "/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data", - "logPath": - "/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774", - "udid": "2706BBEB-1E01-403E-A8E9-70E8E5A24774", - "isAvailable": true, - "deviceTypeIdentifier": - "com.apple.CoreSimulator.SimDeviceType.iPhone-8", - "state": "Shutdown", - "name": "iPhone 8" - }, - { - "dataPath": - "/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data", - "logPath": - "/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A", - "udid": "1E76A0FD-38AC-4537-A989-EA639D7D012A", - "isAvailable": true, - "deviceTypeIdentifier": - "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus", - "state": "Shutdown", - "name": "iPhone 8 Plus" - } - ] - } -}; - -void main() { - const String _kDestination = '--ios-destination'; - const String _kTarget = '--target'; - const String _kSkip = '--skip'; - - group('test xctest_command', () { - CommandRunner runner; - RecordingProcessRunner processRunner; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final XCTestCommand command = XCTestCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner('xctest_command', 'Test for xctest_command'); - runner.addCommand(command); - cleanupPackages(); - }); - - test('Not specifying --target throws', () async { - await expectLater( - () => runner.run(['xctest', _kDestination, 'a_destination']), - throwsA(const TypeMatcher())); - }); - - test('skip if ios is not supported', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - final List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination' - ]); - expect(output, contains('iOS is not supported by this plugin.')); - expect(processRunner.recordedCalls, orderedEquals([])); - - cleanupPackages(); - }); - - test('running with correct scheme and destination, did not find scheme', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - processRunner.resultStdout = '{"project":{"targets":["bar_scheme"]}}'; - - await expectLater(() async { - final List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination' - ]); - expect(output, - contains('foo_scheme not configured for plugin, test failed.')); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('xcrun', ['simctl', 'list', '--json'], null), - ProcessCall( - 'xcodebuild', - [ - '-project', - 'ios/Runner.xcodeproj', - '-list', - '-json' - ], - pluginExampleDirectory.path), - ])); - }, throwsA(const TypeMatcher())); - cleanupPackages(); - }); - - test('running with correct scheme and destination, found scheme', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; - List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination' - ]); - - expect(output, contains('Successfully ran xctest for plugin')); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcodebuild', - ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], - pluginExampleDirectory.path), - ProcessCall( - 'xcodebuild', - [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'foo_scheme', - '-destination', - 'foo_destination', - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ], - pluginExampleDirectory.path), - ])); - - cleanupPackages(); - }); - - test('running with correct scheme and destination, skip 1 plugin', - () async { - createFakePlugin('plugin1', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - createFakePlugin('plugin2', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory1 = - mockPackagesDir.childDirectory('plugin1').childDirectory('example'); - createFakePubspec(pluginExampleDirectory1, isFlutter: true); - final Directory pluginExampleDirectory2 = - mockPackagesDir.childDirectory('plugin2').childDirectory('example'); - createFakePubspec(pluginExampleDirectory2, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; - List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination', - _kSkip, - 'plugin1' - ]); - - expect(output, contains('plugin1 was skipped with the --skip flag.')); - expect(output, contains('Successfully ran xctest for plugin2')); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcodebuild', - ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], - pluginExampleDirectory2.path), - ProcessCall( - 'xcodebuild', - [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'foo_scheme', - '-destination', - 'foo_destination', - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ], - pluginExampleDirectory2.path), - ])); - - cleanupPackages(); - }); - - test('Not specifying --ios-destination assigns an available simulator', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - final Map schemeCommandResult = { - "project": { - "targets": ["bar_scheme", "foo_scheme"] - } - }; - // For simplicity of the test, we combine all the mock results into a single mock result, each internal command - // will get this result and they should still be able to parse them correctly. - processRunner.resultStdout = - jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); - await runner.run([ - 'xctest', - _kTarget, - 'foo_scheme', - ]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('xcrun', ['simctl', 'list', '--json'], null), - ProcessCall( - 'xcodebuild', - ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], - pluginExampleDirectory.path), - ProcessCall( - 'xcodebuild', - [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'foo_scheme', - '-destination', - 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ], - pluginExampleDirectory.path), - ])); - - cleanupPackages(); - }); - }); -} From c5fa26aaf3aa5cbac398f113f0def7d63a24b1ac Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 18:35:42 -0800 Subject: [PATCH 16/27] try run in shell --- .../flutter_plugin_tools/lib/src/build_examples_command.dart | 3 ++- packages/flutter_plugin_tools/lib/src/common.dart | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart index 1493b3cce05d..26a537eb9aac 100644 --- a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart +++ b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart @@ -145,7 +145,8 @@ class BuildExamplesCommand extends PluginCommand { '--enable-experiment=$enableExperiment', ], workingDir: example, - exitOnError: true); + exitOnError: true, + ); if (exitCode != 0) { failingPackages.add('$packageName (ipa)'); } diff --git a/packages/flutter_plugin_tools/lib/src/common.dart b/packages/flutter_plugin_tools/lib/src/common.dart index 78b91ee8a75b..63b358137363 100644 --- a/packages/flutter_plugin_tools/lib/src/common.dart +++ b/packages/flutter_plugin_tools/lib/src/common.dart @@ -388,7 +388,7 @@ class ProcessRunner { print( 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); final io.Process process = await io.Process.start(executable, args, - workingDirectory: workingDir?.path); + workingDirectory: workingDir?.path, runInShell: true); await io.stdout.addStream(process.stdout); await io.stderr.addStream(process.stderr); if (exitOnError && await process.exitCode != 0) { From fcc07823b99eeb2c8a7ebcc8aaa5498fa1074213 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 18:49:11 -0800 Subject: [PATCH 17/27] remove verbose flag --- .../flutter_plugin_tools/lib/src/build_examples_command.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart index 26a537eb9aac..662a9785a260 100644 --- a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart +++ b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart @@ -140,13 +140,12 @@ class BuildExamplesCommand extends PluginCommand { 'build', 'ios', '--no-codesign', - '--verbose', + // '--verbose', if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], workingDir: example, - exitOnError: true, - ); + exitOnError: true,); if (exitCode != 0) { failingPackages.add('$packageName (ipa)'); } From aa9c65c55312fd112198ebba0e795e6071378944 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 19:26:23 -0800 Subject: [PATCH 18/27] clean --- .../flutter_plugin_tools/lib/src/build_examples_command.dart | 2 ++ packages/flutter_plugin_tools/lib/src/common.dart | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart index 662a9785a260..c953a1b8b8e0 100644 --- a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart +++ b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart @@ -133,6 +133,8 @@ class BuildExamplesCommand extends PluginCommand { if (argResults[kIpa]) { print('\nBUILDING IPA for $packageName'); + await processRunner.run(flutterCommand, ['clean']); + await processRunner.run('git', ['clean', '-dfx']); if (isIosPlugin(plugin, fileSystem)) { final int exitCode = await processRunner.runAndStream( flutterCommand, diff --git a/packages/flutter_plugin_tools/lib/src/common.dart b/packages/flutter_plugin_tools/lib/src/common.dart index 63b358137363..f4966a44f440 100644 --- a/packages/flutter_plugin_tools/lib/src/common.dart +++ b/packages/flutter_plugin_tools/lib/src/common.dart @@ -414,6 +414,8 @@ class ProcessRunner { bool exitOnError = false, stdoutEncoding = io.systemEncoding, stderrEncoding = io.systemEncoding}) async { + print( + 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); return io.Process.run(executable, args, workingDirectory: workingDir?.path, stdoutEncoding: stdoutEncoding, From 2c815703de0a7d14e8bab22809ebed92e87b742d Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 19:55:45 -0800 Subject: [PATCH 19/27] log error --- .../lib/src/build_examples_command.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart index c953a1b8b8e0..1f128bedfa15 100644 --- a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart +++ b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart @@ -133,9 +133,15 @@ class BuildExamplesCommand extends PluginCommand { if (argResults[kIpa]) { print('\nBUILDING IPA for $packageName'); - await processRunner.run(flutterCommand, ['clean']); - await processRunner.run('git', ['clean', '-dfx']); if (isIosPlugin(plugin, fileSystem)) { + io.ProcessResult result1 = await processRunner.runAndExitOnError(flutterCommand, ['clean']); + if (result1.exitCode != 0) { + print('flutter clean failed: ${result1.stderr}'); + } + io.ProcessResult result2 = await processRunner.runAndExitOnError('git', ['clean', '-dfx']); + if (result2.exitCode != 0) { + print('git clean failed: ${result1.stderr}'); + } final int exitCode = await processRunner.runAndStream( flutterCommand, [ From b5a53289730d6ff8d80a7d475481deae91676ba9 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 19:57:24 -0800 Subject: [PATCH 20/27] add workingDir --- .../lib/src/build_examples_command.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart index 1f128bedfa15..7c44d7a05bcd 100644 --- a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart +++ b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart @@ -134,11 +134,14 @@ class BuildExamplesCommand extends PluginCommand { if (argResults[kIpa]) { print('\nBUILDING IPA for $packageName'); if (isIosPlugin(plugin, fileSystem)) { - io.ProcessResult result1 = await processRunner.runAndExitOnError(flutterCommand, ['clean']); + io.ProcessResult result1 = await processRunner.runAndExitOnError(flutterCommand, ['clean'], workingDir: example); if (result1.exitCode != 0) { print('flutter clean failed: ${result1.stderr}'); } - io.ProcessResult result2 = await processRunner.runAndExitOnError('git', ['clean', '-dfx']); + io.ProcessResult result2 = await processRunner.runAndExitOnError('git', ['clean', '-dfx'], workingDir: example); + if (result2.exitCode != 0) { + print('git clean failed: ${result1.stderr}'); + } if (result2.exitCode != 0) { print('git clean failed: ${result1.stderr}'); } From 15e4e36b96c32a13e7e101f5b032c67d13c24c1a Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 20:10:47 -0800 Subject: [PATCH 21/27] also print stderr --- .../flutter_plugin_tools/lib/src/build_examples_command.dart | 3 --- packages/flutter_plugin_tools/lib/src/common.dart | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart index 7c44d7a05bcd..257ea0479456 100644 --- a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart +++ b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart @@ -142,9 +142,6 @@ class BuildExamplesCommand extends PluginCommand { if (result2.exitCode != 0) { print('git clean failed: ${result1.stderr}'); } - if (result2.exitCode != 0) { - print('git clean failed: ${result1.stderr}'); - } final int exitCode = await processRunner.runAndStream( flutterCommand, [ diff --git a/packages/flutter_plugin_tools/lib/src/common.dart b/packages/flutter_plugin_tools/lib/src/common.dart index f4966a44f440..657d2b8d1dc8 100644 --- a/packages/flutter_plugin_tools/lib/src/common.dart +++ b/packages/flutter_plugin_tools/lib/src/common.dart @@ -454,7 +454,7 @@ class ProcessRunner { if (result.exitCode != 0) { final String error = _getErrorString(executable, args, workingDir: workingDir); - print('$error Stderr:\n${result.stdout}'); + print('$error stdout:\n${result.stdout} \n stderr: ${result.stderr}'); throw ToolExit(result.exitCode); } return result; From b2586d247385728a257f9390371211022d4da158 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 20:12:16 -0800 Subject: [PATCH 22/27] print command --- packages/flutter_plugin_tools/lib/src/common.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/flutter_plugin_tools/lib/src/common.dart b/packages/flutter_plugin_tools/lib/src/common.dart index 657d2b8d1dc8..94e50c8d4c0e 100644 --- a/packages/flutter_plugin_tools/lib/src/common.dart +++ b/packages/flutter_plugin_tools/lib/src/common.dart @@ -449,6 +449,8 @@ class ProcessRunner { List args, { Directory workingDir, }) async { + print( + 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); final io.ProcessResult result = await io.Process.run(executable, args, workingDirectory: workingDir?.path); if (result.exitCode != 0) { From 63b97489058b465b14c48b15ae8b2a677f1465b9 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 20:13:56 -0800 Subject: [PATCH 23/27] swap command --- .../lib/src/build_examples_command.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart index 257ea0479456..cdee9899a4c4 100644 --- a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart +++ b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart @@ -134,13 +134,17 @@ class BuildExamplesCommand extends PluginCommand { if (argResults[kIpa]) { print('\nBUILDING IPA for $packageName'); if (isIosPlugin(plugin, fileSystem)) { - io.ProcessResult result1 = await processRunner.runAndExitOnError(flutterCommand, ['clean'], workingDir: example); - if (result1.exitCode != 0) { - print('flutter clean failed: ${result1.stderr}'); + io.ProcessResult result0 = await processRunner.runAndExitOnError('ls', ['-1'], workingDir: example); + if (result0.exitCode != 0) { + print('ls -l failed: ${result0.stderr}'); } io.ProcessResult result2 = await processRunner.runAndExitOnError('git', ['clean', '-dfx'], workingDir: example); if (result2.exitCode != 0) { - print('git clean failed: ${result1.stderr}'); + print('git clean failed: ${result2.stderr}'); + } + io.ProcessResult result1 = await processRunner.runAndExitOnError(flutterCommand, ['clean'], workingDir: example); + if (result1.exitCode != 0) { + print('flutter clean failed: ${result1.stderr}'); } final int exitCode = await processRunner.runAndStream( flutterCommand, From 77d5859fcb4e877dc4eace4ad48bcf3e1a6383c3 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 27 Jan 2021 20:23:36 -0800 Subject: [PATCH 24/27] ls -l after flutter clean --- .../lib/src/build_examples_command.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart index cdee9899a4c4..742487250904 100644 --- a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart +++ b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart @@ -134,10 +134,6 @@ class BuildExamplesCommand extends PluginCommand { if (argResults[kIpa]) { print('\nBUILDING IPA for $packageName'); if (isIosPlugin(plugin, fileSystem)) { - io.ProcessResult result0 = await processRunner.runAndExitOnError('ls', ['-1'], workingDir: example); - if (result0.exitCode != 0) { - print('ls -l failed: ${result0.stderr}'); - } io.ProcessResult result2 = await processRunner.runAndExitOnError('git', ['clean', '-dfx'], workingDir: example); if (result2.exitCode != 0) { print('git clean failed: ${result2.stderr}'); @@ -146,6 +142,10 @@ class BuildExamplesCommand extends PluginCommand { if (result1.exitCode != 0) { print('flutter clean failed: ${result1.stderr}'); } + io.ProcessResult result0 = await processRunner.runAndExitOnError('ls', ['-1'], workingDir: example); + if (result0.exitCode != 0) { + print('ls -l failed: ${result0.stderr}'); + } final int exitCode = await processRunner.runAndStream( flutterCommand, [ From ee5e500135576502a6ecebe52b7b0963ebc2adac Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 28 Jan 2021 11:20:53 -0800 Subject: [PATCH 25/27] upgrade flutter before running into script --- .cirrus.yml | 3 +- packages/flutter_plugin_tools/CHANGELOG.md | 288 ---------- packages/flutter_plugin_tools/LICENSE | 29 - packages/flutter_plugin_tools/README.md | 45 -- .../analysis_options.yaml | 1 - .../bin/flutter_plugin_tools.dart | 5 - .../lib/src/analyze_command.dart | 94 ---- .../lib/src/build_examples_command.dart | 203 ------- .../flutter_plugin_tools/lib/src/common.dart | 470 ---------------- .../src/create_all_plugins_app_command.dart | 200 ------- .../lib/src/drive_examples_command.dart | 210 -------- .../lib/src/firebase_test_lab_command.dart | 264 --------- .../lib/src/format_command.dart | 147 ----- .../lib/src/java_test_command.dart | 89 --- .../lib/src/lint_podspecs_command.dart | 146 ----- .../lib/src/list_command.dart | 60 --- .../flutter_plugin_tools/lib/src/main.dart | 63 --- .../lib/src/publish_plugin_command.dart | 227 -------- .../lib/src/test_command.dart | 101 ---- .../lib/src/version_check_command.dart | 220 -------- .../lib/src/xctest_command.dart | 216 -------- packages/flutter_plugin_tools/pubspec.yaml | 30 -- .../test/analyze_command_test.dart | 93 ---- .../test/build_examples_command_test.dart | 470 ---------------- .../test/common_test.dart | 100 ---- .../test/drive_examples_command_test.dart | 505 ------------------ .../test/firebase_test_lab_test.dart | 256 --------- .../test/lint_podspecs_command_test.dart | 202 ------- .../test/list_command_test.dart | 198 ------- packages/flutter_plugin_tools/test/mocks.dart | 33 -- .../test/publish_plugin_command_test.dart | 359 ------------- .../test/test_command_test.dart | 154 ------ packages/flutter_plugin_tools/test/util.dart | 291 ---------- .../test/version_check_test.dart | 319 ----------- .../test/xctest_command_test.dart | 358 ------------- 35 files changed, 2 insertions(+), 6447 deletions(-) delete mode 100644 packages/flutter_plugin_tools/CHANGELOG.md delete mode 100644 packages/flutter_plugin_tools/LICENSE delete mode 100644 packages/flutter_plugin_tools/README.md delete mode 100644 packages/flutter_plugin_tools/analysis_options.yaml delete mode 100644 packages/flutter_plugin_tools/bin/flutter_plugin_tools.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/analyze_command.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/build_examples_command.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/common.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/create_all_plugins_app_command.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/drive_examples_command.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/firebase_test_lab_command.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/format_command.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/java_test_command.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/lint_podspecs_command.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/list_command.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/main.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/publish_plugin_command.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/test_command.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/version_check_command.dart delete mode 100644 packages/flutter_plugin_tools/lib/src/xctest_command.dart delete mode 100644 packages/flutter_plugin_tools/pubspec.yaml delete mode 100644 packages/flutter_plugin_tools/test/analyze_command_test.dart delete mode 100644 packages/flutter_plugin_tools/test/build_examples_command_test.dart delete mode 100644 packages/flutter_plugin_tools/test/common_test.dart delete mode 100644 packages/flutter_plugin_tools/test/drive_examples_command_test.dart delete mode 100644 packages/flutter_plugin_tools/test/firebase_test_lab_test.dart delete mode 100644 packages/flutter_plugin_tools/test/lint_podspecs_command_test.dart delete mode 100644 packages/flutter_plugin_tools/test/list_command_test.dart delete mode 100644 packages/flutter_plugin_tools/test/mocks.dart delete mode 100644 packages/flutter_plugin_tools/test/publish_plugin_command_test.dart delete mode 100644 packages/flutter_plugin_tools/test/test_command_test.dart delete mode 100644 packages/flutter_plugin_tools/test/util.dart delete mode 100644 packages/flutter_plugin_tools/test/version_check_test.dart delete mode 100644 packages/flutter_plugin_tools/test/xctest_command_test.dart diff --git a/.cirrus.yml b/.cirrus.yml index c6a199b0f260..1f82270163fb 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -146,7 +146,7 @@ task: - flutter channel master - flutter upgrade - git fetch origin master - activate_script: pub global activate -s path ./packages/flutter_plugin_tools + activate_script: pub global activate flutter_plugin_tools create_simulator_script: - xcrun simctl list - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-3 | xargs xcrun simctl boot @@ -191,6 +191,7 @@ task: # https://github.com/flutter/flutter/issues/42864 - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - flutter channel $CHANNEL + - flutter upgrade - ./script/incremental_build.sh build-examples --ipa - ./script/incremental_build.sh drive-examples - ./script/incremental_build.sh xctest --target RunnerUITests --skip $PLUGINS_TO_SKIP_XCTESTS diff --git a/packages/flutter_plugin_tools/CHANGELOG.md b/packages/flutter_plugin_tools/CHANGELOG.md deleted file mode 100644 index 25dce424eee4..000000000000 --- a/packages/flutter_plugin_tools/CHANGELOG.md +++ /dev/null @@ -1,288 +0,0 @@ -## v.0.0.45+1 - -- Don't call `flutter format` if there are no Dart files to format. - -## v.0.0.45 - -- Add exclude flag to exclude any plugin from further processing. - -## v.0.0.44+7 - -- `all-plugins-app` doesn't override the AGP version. - -## v.0.0.44+6 - -- Fix code formatting. - -## v.0.0.44+5 - -- Remove `-v` flag on drive-examples. - -## v.0.0.44+4 - -- Fix bug where directory isn't passed - -## v.0.0.44+3 - -- More verbose logging - -## v.0.0.44+2 - -- Remove pre-alpha Windows workaround to create examples on the fly. - -## v.0.0.44+1 - -- Print packages that passed tests in `xctest` command. -- Remove printing the whole list of simulators. - -## v.0.0.44 - -- Add 'xctest' command to run xctests. - -## v.0.0.43 - -- Allow minor `*-nullsafety` pre release packages. - -## v.0.0.42+1 - -- Fix test command when `--enable-experiment` is called. - -## v.0.0.42 - -- Allow `*-nullsafety` pre release packages. - -## v.0.0.41 - -- Support `--enable-experiment` flag in subcommands `test`, `build-examples`, `drive-examples`, -and `firebase-test-lab`. - -## v.0.0.40 - -- Support `integration_test/` directory for `drive-examples` command - -## v.0.0.39 - -- Support `integration_test/` directory for `package:integration_test` - -## v.0.0.38 - -- Add C++ and ObjC++ to clang-format. - -## v.0.0.37+2 - -- Make `http` and `http_multi_server` dependency version constraint more flexible. - -## v.0.0.37+1 - -- All_plugin test puts the plugin dependencies into dependency_overrides. - -## v.0.0.37 - -- Only builds mobile example apps when necessary. - -## v.0.0.36+3 - -- Add support for Linux plugins. - -## v.0.0.36+2 - -- Default to showing podspec lint warnings - -## v.0.0.36+1 - -- Serialize linting podspecs. - -## v.0.0.36 - -- Remove retry on Firebase Test Lab's call to gcloud set. -- Remove quiet flag from Firebase Test Lab's gcloud set command. -- Allow Firebase Test Lab command to continue past gcloud set network failures. - This is a mitigation for the network service sometimes not responding, - but it isn't actually necessary to have a network connection for this command. - -## v.0.0.35+1 - -- Minor cleanup to the analyze test. - -## v.0.0.35 - -- Firebase Test Lab command generates a configurable unique path suffix for results. - -## v.0.0.34 - -- Firebase Test Lab command now only tries to configure the project once -- Firebase Test Lab command now retries project configuration up to five times. - -## v.0.0.33+1 - -- Fixes formatting issues that got past our CI due to - https://github.com/flutter/flutter/issues/51585. -- Changes the default package name for testing method `createFakePubspec` back - its previous behavior. - -## v.0.0.33 - -- Version check command now fails on breaking changes to platform interfaces. -- Updated version check test to be more flexible. - -## v.0.0.32+7 - -- Ensure that Firebase Test Lab tests have a unique storage bucket for each test run. - -## v.0.0.32+6 - -- Ensure that Firebase Test Lab tests have a unique storage bucket for each package. - -## v.0.0.32+5 - -- Remove --fail-fast and --silent from lint podspec command. - -## v.0.0.32+4 - -- Update `publish-plugin` to use `flutter pub publish` instead of just `pub - publish`. Enforces a `pub publish` command that matches the Dart SDK in the - user's Flutter install. - -## v.0.0.32+3 - -- Update Firebase Testlab deprecated test device. (Pixel 3 API 28 -> Pixel 4 API 29). - -## v.0.0.32+2 - -- Runs pub get before building macos to avoid failures. - -## v.0.0.32+1 - -- Default macOS example builds to false. Previously they were running whenever - CI was itself running on macOS. - -## v.0.0.32 - -- `analyze` now asserts that the global `analysis_options.yaml` is the only one - by default. Individual directories can be excluded from this check with the - new `--custom-analysis` flag. - -## v.0.0.31+1 - -- Add --skip and --no-analyze flags to podspec command. - -## v.0.0.31 - -- Add support for macos on `DriveExamplesCommand` and `BuildExamplesCommand`. - -## v.0.0.30 - -- Adopt pedantic analysis options, fix firebase_test_lab_test. - -## v.0.0.29 - -- Add a command to run pod lib lint on podspec files. - -## v.0.0.28 - -- Increase Firebase test lab timeouts to 5 minutes. - -## v.0.0.27 - -- Run tests with `--platform=chrome` for web plugins. - -## v.0.0.26 - -- Add a command for publishing plugins to pub. - -## v.0.0.25 - -- Update `DriveExamplesCommand` to use `ProcessRunner`. -- Make `DriveExamplesCommand` rely on `ProcessRunner` to determine if the test fails or not. -- Add simple tests for `DriveExamplesCommand`. - -## v.0.0.24 - -- Gracefully handle pubspec.yaml files for new plugins. -- Additional unit testing. - -## v.0.0.23 - -- Add a test case for transitive dependency solving in the - `create_all_plugins_app` command. - -## v.0.0.22 - -- Updated firebase-test-lab command with updated conventions for test locations. -- Updated firebase-test-lab to add an optional "device" argument. -- Updated version-check command to always compare refs instead of using the working copy. -- Added unit tests for the firebase-test-lab and version-check commands. -- Add ProcessRunner to mock running processes for testing. - -## v.0.0.21 - -- Support the `--plugins` argument for federated plugins. - -## v.0.0.20 - -- Support for finding federated plugins, where one directory contains - multiple packages for different platform implementations. - -## v.0.0.19+3 - -- Use `package:file` for file I/O. - -## v.0.0.19+2 - -- Use java as language when calling `flutter create`. - -## v.0.0.19+1 - -- Rename command for `CreateAllPluginsAppCommand`. - -## v.0.0.19 - -- Use flutter create to build app testing plugin compilation. - -## v.0.0.18+2 - -- Fix `.travis.yml` file name in `README.md`. - -## v0.0.18+1 - -- Skip version check if it contains `publish_to: none`. - -## v0.0.18 - -- Add option to exclude packages from generated pubspec command. - -## v0.0.17+4 - -- Avoid trying to version-check pubspecs that are missing a version. - -## v0.0.17+3 - -- version-check accounts for [pre-1.0 patch versions](https://github.com/flutter/flutter/issues/35412). - -## v0.0.17+2 - -- Fix exception handling for version checker - -## v0.0.17+1 - -- Fix bug where we used a flag instead of an option - -## v0.0.17 - -- Add a command for checking the version number - -## v0.0.16 - -- Add a command for generating `pubspec.yaml` for All Plugins app. - -## v0.0.15 - -- Add a command for running driver tests of plugin examples. - -## v0.0.14 - -- Check for dependencies->flutter instead of top level flutter node. - -## v0.0.13 - -- Differentiate between Flutter and non-Flutter (but potentially Flutter consumed) Dart packages. diff --git a/packages/flutter_plugin_tools/LICENSE b/packages/flutter_plugin_tools/LICENSE deleted file mode 100644 index 5f015bc7b321..000000000000 --- a/packages/flutter_plugin_tools/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2017, Flutter -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/flutter_plugin_tools/README.md b/packages/flutter_plugin_tools/README.md deleted file mode 100644 index efdc668e81a0..000000000000 --- a/packages/flutter_plugin_tools/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Flutter Plugin Tools - -[![Build Status](https://travis-ci.org/flutter/plugin_tools.svg?branch=master)](https://travis-ci.org/flutter/plugin_tools) -[![pub package](https://img.shields.io/pub/v/flutter_plugin_tools.svg)](https://pub.dartlang.org/packages/flutter_plugin_tools) - - -Flutter Plugin Tools implements a CLI with various productivity tools for hosting multiple Flutter plugins in one github -repository. It is mainly used by the [flutter/plugins](https://github.com/flutter/plugins) and -[flutter/flutterfire](https://github.com/flutter/flutterfire) repositories. It was mainly written to facilitate -testing on Travis for these repositories (see [.travis.yml](https://github.com/flutter/plugins/blob/master/.travis.yml)). - -As an example, Flutter Plugin Tools allows you to: - -* Build all plugin example apps with one command -* Run the tests of all plugins with one command -* Format all Dart, Java, Objective-C, and C++ code in the repository -* Define shards of the above tasks - -## Installation - -In order to use the tools you need to enable them once by running the following command: - -```shell -$ pub global activate flutter_plugin_tools -``` - -## Requirements - -To use all features of `flutter_plugin_tools` you'll need the following commands in your `PATH`: -* `flutter` -* `git` -* `pub` (recommended: version from `/bin/cache/dart-sdk/bin`) -* `clang-format` version 5 (alternatively, you can provide the path via `--clang-format=`) -* [`pod`](https://guides.cocoapods.org/using/getting-started.html#installation) (macOS only) - -## Usage - -```shell -$ pub global run flutter_plugin_tools -$ pub global run flutter_plugin_tools --shardIndex 0 --shardCount 3 -``` - -Run commands from the `flutter/plugins` directory. Replace `` with `help` to print a list of available commands. -The sharded example above divides the plugins into three shards -and executes the tool on the first shard (index 0). diff --git a/packages/flutter_plugin_tools/analysis_options.yaml b/packages/flutter_plugin_tools/analysis_options.yaml deleted file mode 100644 index 84a5e26f95de..000000000000 --- a/packages/flutter_plugin_tools/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: package:pedantic/analysis_options.1.8.0.yaml diff --git a/packages/flutter_plugin_tools/bin/flutter_plugin_tools.dart b/packages/flutter_plugin_tools/bin/flutter_plugin_tools.dart deleted file mode 100644 index 43edcbfe2c68..000000000000 --- a/packages/flutter_plugin_tools/bin/flutter_plugin_tools.dart +++ /dev/null @@ -1,5 +0,0 @@ -// 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. - -export 'package:flutter_plugin_tools/src/main.dart'; diff --git a/packages/flutter_plugin_tools/lib/src/analyze_command.dart b/packages/flutter_plugin_tools/lib/src/analyze_command.dart deleted file mode 100644 index 8cd57fa0b338..000000000000 --- a/packages/flutter_plugin_tools/lib/src/analyze_command.dart +++ /dev/null @@ -1,94 +0,0 @@ -// 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:file/file.dart'; -import 'package:path/path.dart' as p; - -import 'common.dart'; - -class AnalyzeCommand extends PluginCommand { - AnalyzeCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addMultiOption(_customAnalysisFlag, - help: - 'Directories (comma seperated) that are allowed to have their own analysis options.', - defaultsTo: []); - } - - static const String _customAnalysisFlag = 'custom-analysis'; - - @override - final String name = 'analyze'; - - @override - final String description = 'Analyzes all packages using package:tuneup.\n\n' - 'This command requires "pub" and "flutter" to be in your path.'; - - @override - Future run() async { - checkSharding(); - - print('Verifying analysis settings...'); - final List files = packagesDir.listSync(recursive: true); - for (final FileSystemEntity file in files) { - if (file.basename != 'analysis_options.yaml' && - file.basename != '.analysis_options') { - continue; - } - - final bool whitelisted = argResults[_customAnalysisFlag].any( - (String directory) => - p.isWithin(p.join(packagesDir.path, directory), file.path)); - if (whitelisted) { - continue; - } - - print('Found an extra analysis_options.yaml in ${file.absolute.path}.'); - print( - 'If this was deliberate, pass the package to the analyze command with the --$_customAnalysisFlag flag and try again.'); - throw ToolExit(1); - } - - print('Activating tuneup package...'); - await processRunner.runAndStream( - 'pub', ['global', 'activate', 'tuneup'], - workingDir: packagesDir, exitOnError: true); - - await for (Directory package in getPackages()) { - if (isFlutterPackage(package, fileSystem)) { - await processRunner.runAndStream('flutter', ['packages', 'get'], - workingDir: package, exitOnError: true); - } else { - await processRunner.runAndStream('pub', ['get'], - workingDir: package, exitOnError: true); - } - } - - final List failingPackages = []; - await for (Directory package in getPlugins()) { - final int exitCode = await processRunner.runAndStream( - 'pub', ['global', 'run', 'tuneup', 'check'], - workingDir: package); - if (exitCode != 0) { - failingPackages.add(p.basename(package.path)); - } - } - - print('\n\n'); - if (failingPackages.isNotEmpty) { - print('The following packages have analyzer errors (see above):'); - failingPackages.forEach((String package) { - print(' * $package'); - }); - throw ToolExit(1); - } - - print('No analyzer errors found!'); - } -} diff --git a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart b/packages/flutter_plugin_tools/lib/src/build_examples_command.dart deleted file mode 100644 index 742487250904..000000000000 --- a/packages/flutter_plugin_tools/lib/src/build_examples_command.dart +++ /dev/null @@ -1,203 +0,0 @@ -// 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 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; - -import 'common.dart'; - -class BuildExamplesCommand extends PluginCommand { - BuildExamplesCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addFlag(kLinux, defaultsTo: false); - argParser.addFlag(kMacos, defaultsTo: false); - argParser.addFlag(kWindows, defaultsTo: false); - argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS); - argParser.addFlag(kApk); - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: 'Enables the given Dart SDK experiments.', - ); - } - - @override - final String name = 'build-examples'; - - @override - final String description = - 'Builds all example apps (IPA for iOS and APK for Android).\n\n' - 'This command requires "flutter" to be in your path.'; - - @override - Future run() async { - if (!argResults[kIpa] && - !argResults[kApk] && - !argResults[kLinux] && - !argResults[kMacos] && - !argResults[kWindows]) { - print( - 'None of --linux, --macos, --windows, --apk nor --ipa were specified, ' - 'so not building anything.'); - return; - } - final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - - final String enableExperiment = argResults[kEnableExperiment]; - - checkSharding(); - final List failingPackages = []; - await for (Directory plugin in getPlugins()) { - for (Directory example in getExamplesForPlugin(plugin)) { - final String packageName = - p.relative(example.path, from: packagesDir.path); - - if (argResults[kLinux]) { - print('\nBUILDING Linux for $packageName'); - if (isLinuxPlugin(plugin, fileSystem)) { - int buildExitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kLinux, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (buildExitCode != 0) { - failingPackages.add('$packageName (linux)'); - } - } else { - print('Linux is not supported by this plugin'); - } - } - - if (argResults[kMacos]) { - print('\nBUILDING macOS for $packageName'); - if (isMacOsPlugin(plugin, fileSystem)) { - // TODO(https://github.com/flutter/flutter/issues/46236): - // Builing macos without running flutter pub get first results - // in an error. - int exitCode = await processRunner.runAndStream( - flutterCommand, ['pub', 'get'], - workingDir: example); - if (exitCode != 0) { - failingPackages.add('$packageName (macos)'); - } else { - exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kMacos, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (exitCode != 0) { - failingPackages.add('$packageName (macos)'); - } - } - } else { - print('macOS is not supported by this plugin'); - } - } - - if (argResults[kWindows]) { - print('\nBUILDING Windows for $packageName'); - if (isWindowsPlugin(plugin, fileSystem)) { - int buildExitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kWindows, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (buildExitCode != 0) { - failingPackages.add('$packageName (windows)'); - } - } else { - print('Windows is not supported by this plugin'); - } - } - - if (argResults[kIpa]) { - print('\nBUILDING IPA for $packageName'); - if (isIosPlugin(plugin, fileSystem)) { - io.ProcessResult result2 = await processRunner.runAndExitOnError('git', ['clean', '-dfx'], workingDir: example); - if (result2.exitCode != 0) { - print('git clean failed: ${result2.stderr}'); - } - io.ProcessResult result1 = await processRunner.runAndExitOnError(flutterCommand, ['clean'], workingDir: example); - if (result1.exitCode != 0) { - print('flutter clean failed: ${result1.stderr}'); - } - io.ProcessResult result0 = await processRunner.runAndExitOnError('ls', ['-1'], workingDir: example); - if (result0.exitCode != 0) { - print('ls -l failed: ${result0.stderr}'); - } - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - 'ios', - '--no-codesign', - // '--verbose', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example, - exitOnError: true,); - if (exitCode != 0) { - failingPackages.add('$packageName (ipa)'); - } - } else { - print('iOS is not supported by this plugin'); - } - } - print('end ios build'); - - if (argResults[kApk]) { - print('\nBUILDING APK for $packageName'); - if (isAndroidPlugin(plugin, fileSystem)) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - 'apk', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (exitCode != 0) { - failingPackages.add('$packageName (apk)'); - } - } else { - print('Android is not supported by this plugin'); - } - } - } - } - print('\n\n'); - - if (failingPackages.isNotEmpty) { - print('The following build are failing (see above for details):'); - for (String package in failingPackages) { - print(' * $package'); - } - throw ToolExit(1); - } - - print('All builds successful!'); - } -} diff --git a/packages/flutter_plugin_tools/lib/src/common.dart b/packages/flutter_plugin_tools/lib/src/common.dart deleted file mode 100644 index 94e50c8d4c0e..000000000000 --- a/packages/flutter_plugin_tools/lib/src/common.dart +++ /dev/null @@ -1,470 +0,0 @@ -// 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 'dart:io' as io; -import 'dart:math'; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:yaml/yaml.dart'; - -typedef void Print(Object object); - -/// Key for windows platform. -const String kWindows = 'windows'; - -/// Key for macos platform. -const String kMacos = 'macos'; - -/// Key for linux platform. -const String kLinux = 'linux'; - -/// Key for IPA (iOS) platform. -const String kIos = 'ios'; - -/// Key for APK (Android) platform. -const String kAndroid = 'android'; - -/// Key for Web platform. -const String kWeb = 'web'; - -/// Key for IPA. -const String kIpa = 'ipa'; - -/// Key for APK. -const String kApk = 'apk'; - -/// Key for enable experiment. -const String kEnableExperiment = 'enable-experiment'; - -/// Returns whether the given directory contains a Flutter package. -bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) { - if (entity == null || entity is! Directory) { - return false; - } - - try { - final File pubspecFile = - fileSystem.file(p.join(entity.path, 'pubspec.yaml')); - final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); - final YamlMap dependencies = pubspecYaml['dependencies']; - if (dependencies == null) { - return false; - } - return dependencies.containsKey('flutter'); - } on FileSystemException { - return false; - } on YamlException { - return false; - } -} - -/// Returns whether the given directory contains a Flutter [platform] plugin. -/// -/// It checks this by looking for the following pattern in the pubspec: -/// -/// flutter: -/// plugin: -/// platforms: -/// [platform]: -bool pluginSupportsPlatform( - String platform, FileSystemEntity entity, FileSystem fileSystem) { - assert(platform == kIos || - platform == kAndroid || - platform == kWeb || - platform == kMacos || - platform == kWindows || - platform == kLinux); - if (entity == null || entity is! Directory) { - return false; - } - - try { - final File pubspecFile = - fileSystem.file(p.join(entity.path, 'pubspec.yaml')); - final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); - final YamlMap flutterSection = pubspecYaml['flutter']; - if (flutterSection == null) { - return false; - } - final YamlMap pluginSection = flutterSection['plugin']; - if (pluginSection == null) { - return false; - } - final YamlMap platforms = pluginSection['platforms']; - if (platforms == null) { - // Legacy plugin specs are assumed to support iOS and Android. - if (!pluginSection.containsKey('platforms')) { - return platform == kIos || platform == kAndroid; - } - return false; - } - return platforms.containsKey(platform); - } on FileSystemException { - return false; - } on YamlException { - return false; - } -} - -/// Returns whether the given directory contains a Flutter Android plugin. -bool isAndroidPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kAndroid, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter iOS plugin. -bool isIosPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kIos, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter web plugin. -bool isWebPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kWeb, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter Windows plugin. -bool isWindowsPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kWindows, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter macOS plugin. -bool isMacOsPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kMacos, entity, fileSystem); -} - -/// Returns whether the given directory contains a Flutter linux plugin. -bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kLinux, entity, fileSystem); -} - -/// Error thrown when a command needs to exit with a non-zero exit code. -class ToolExit extends Error { - ToolExit(this.exitCode); - - final int exitCode; -} - -abstract class PluginCommand extends Command { - PluginCommand( - this.packagesDir, - this.fileSystem, { - this.processRunner = const ProcessRunner(), - }) { - argParser.addMultiOption( - _pluginsArg, - splitCommas: true, - help: - 'Specifies which plugins the command should run on (before sharding).', - valueHelp: 'plugin1,plugin2,...', - ); - argParser.addOption( - _shardIndexArg, - help: 'Specifies the zero-based index of the shard to ' - 'which the command applies.', - valueHelp: 'i', - defaultsTo: '0', - ); - argParser.addOption( - _shardCountArg, - help: 'Specifies the number of shards into which plugins are divided.', - valueHelp: 'n', - defaultsTo: '1', - ); - argParser.addMultiOption( - _excludeArg, - abbr: 'e', - help: 'Exclude packages from this command.', - defaultsTo: [], - ); - } - - static const String _pluginsArg = 'plugins'; - static const String _shardIndexArg = 'shardIndex'; - static const String _shardCountArg = 'shardCount'; - static const String _excludeArg = 'exclude'; - - /// The directory containing the plugin packages. - final Directory packagesDir; - - /// The file system. - /// - /// This can be overridden for testing. - final FileSystem fileSystem; - - /// The process runner. - /// - /// This can be overridden for testing. - final ProcessRunner processRunner; - - int _shardIndex; - int _shardCount; - - int get shardIndex { - if (_shardIndex == null) { - checkSharding(); - } - return _shardIndex; - } - - int get shardCount { - if (_shardCount == null) { - checkSharding(); - } - return _shardCount; - } - - void checkSharding() { - final int shardIndex = int.tryParse(argResults[_shardIndexArg]); - final int shardCount = int.tryParse(argResults[_shardCountArg]); - if (shardIndex == null) { - usageException('$_shardIndexArg must be an integer'); - } - if (shardCount == null) { - usageException('$_shardCountArg must be an integer'); - } - if (shardCount < 1) { - usageException('$_shardCountArg must be positive'); - } - if (shardIndex < 0 || shardCount <= shardIndex) { - usageException( - '$_shardIndexArg must be in the half-open range [0..$shardCount['); - } - _shardIndex = shardIndex; - _shardCount = shardCount; - } - - /// Returns the root Dart package folders of the plugins involved in this - /// command execution. - Stream getPlugins() async* { - // To avoid assuming consistency of `Directory.list` across command - // invocations, we collect and sort the plugin folders before sharding. - // This is considered an implementation detail which is why the API still - // uses streams. - final List allPlugins = await _getAllPlugins().toList(); - allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path)); - // Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2. - // Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3. - // Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0. - final int shardSize = allPlugins.length ~/ shardCount + - (allPlugins.length % shardCount == 0 ? 0 : 1); - final int start = min(shardIndex * shardSize, allPlugins.length); - final int end = min(start + shardSize, allPlugins.length); - - for (Directory plugin in allPlugins.sublist(start, end)) { - yield plugin; - } - } - - /// Returns the root Dart package folders of the plugins involved in this - /// command execution, assuming there is only one shard. - /// - /// Plugin packages can exist in one of two places relative to the packages - /// directory. - /// - /// 1. As a Dart package in a directory which is a direct child of the - /// packages directory. This is a plugin where all of the implementations - /// exist in a single Dart package. - /// 2. Several plugin packages may live in a directory which is a direct - /// child of the packages directory. This directory groups several Dart - /// packages which implement a single plugin. This directory contains a - /// "client library" package, which declares the API for the plugin, as - /// well as one or more platform-specific implementations. - Stream _getAllPlugins() async* { - final Set plugins = Set.from(argResults[_pluginsArg]); - final Set excludedPlugins = - Set.from(argResults[_excludeArg]); - - await for (FileSystemEntity entity - in packagesDir.list(followLinks: false)) { - // A top-level Dart package is a plugin package. - if (_isDartPackage(entity)) { - if (!excludedPlugins.contains(entity.basename) && - (plugins.isEmpty || plugins.contains(p.basename(entity.path)))) { - yield entity; - } - } else if (entity is Directory) { - // Look for Dart packages under this top-level directory. - await for (FileSystemEntity subdir in entity.list(followLinks: false)) { - if (_isDartPackage(subdir)) { - // If --plugin=my_plugin is passed, then match all federated - // plugins under 'my_plugin'. Also match if the exact plugin is - // passed. - final String relativePath = - p.relative(subdir.path, from: packagesDir.path); - final String basenamePath = p.basename(entity.path); - if (!excludedPlugins.contains(basenamePath) && - !excludedPlugins.contains(relativePath) && - (plugins.isEmpty || - plugins.contains(relativePath) || - plugins.contains(basenamePath))) { - yield subdir; - } - } - } - } - } - } - - /// Returns the example Dart package folders of the plugins involved in this - /// command execution. - Stream getExamples() => - getPlugins().expand(getExamplesForPlugin); - - /// Returns all Dart package folders (typically, plugin + example) of the - /// plugins involved in this command execution. - Stream getPackages() async* { - await for (Directory plugin in getPlugins()) { - yield plugin; - yield* plugin - .list(recursive: true, followLinks: false) - .where(_isDartPackage) - .cast(); - } - } - - /// Returns the files contained, recursively, within the plugins - /// involved in this command execution. - Stream getFiles() { - return getPlugins().asyncExpand((Directory folder) => folder - .list(recursive: true, followLinks: false) - .where((FileSystemEntity entity) => entity is File) - .cast()); - } - - /// Returns whether the specified entity is a directory containing a - /// `pubspec.yaml` file. - bool _isDartPackage(FileSystemEntity entity) { - return entity is Directory && - fileSystem.file(p.join(entity.path, 'pubspec.yaml')).existsSync(); - } - - /// Returns the example Dart packages contained in the specified plugin, or - /// an empty List, if the plugin has no examples. - Iterable getExamplesForPlugin(Directory plugin) { - final Directory exampleFolder = - fileSystem.directory(p.join(plugin.path, 'example')); - if (!exampleFolder.existsSync()) { - return []; - } - if (isFlutterPackage(exampleFolder, fileSystem)) { - return [exampleFolder]; - } - // Only look at the subdirectories of the example directory if the example - // directory itself is not a Dart package, and only look one level below the - // example directory for other dart packages. - return exampleFolder - .listSync() - .where( - (FileSystemEntity entity) => isFlutterPackage(entity, fileSystem)) - .cast(); - } -} - -/// A class used to run processes. -/// -/// We use this instead of directly running the process so it can be overridden -/// in tests. -class ProcessRunner { - const ProcessRunner(); - - /// Run the [executable] with [args] and stream output to stderr and stdout. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// If [exitOnError] is set to `true`, then this will throw an error if - /// the [executable] terminates with a non-zero exit code. - /// - /// Returns the exit code of the [executable]. - Future runAndStream( - String executable, - List args, { - Directory workingDir, - bool exitOnError = false, - }) async { - print( - 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); - final io.Process process = await io.Process.start(executable, args, - workingDirectory: workingDir?.path, runInShell: true); - await io.stdout.addStream(process.stdout); - await io.stderr.addStream(process.stderr); - if (exitOnError && await process.exitCode != 0) { - final String error = - _getErrorString(executable, args, workingDir: workingDir); - print('$error See above for details.'); - throw ToolExit(await process.exitCode); - } - return process.exitCode; - } - - /// Run the [executable] with [args]. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// If [exitOnError] is set to `true`, then this will throw an error if - /// the [executable] terminates with a non-zero exit code. - /// - /// Returns the [io.ProcessResult] of the [executable]. - Future run(String executable, List args, - {Directory workingDir, - bool exitOnError = false, - stdoutEncoding = io.systemEncoding, - stderrEncoding = io.systemEncoding}) async { - print( - 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); - return io.Process.run(executable, args, - workingDirectory: workingDir?.path, - stdoutEncoding: stdoutEncoding, - stderrEncoding: stderrEncoding); - } - - /// Starts the [executable] with [args]. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// Returns the started [io.Process]. - Future start(String executable, List args, - {Directory workingDirectory}) async { - final io.Process process = await io.Process.start(executable, args, - workingDirectory: workingDirectory?.path); - return process; - } - - /// Run the [executable] with [args], throwing an error on non-zero exit code. - /// - /// Unlike [runAndStream], this does not stream the process output to stdout. - /// It also unconditionally throws an error on a non-zero exit code. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// Returns the [io.ProcessResult] of running the [executable]. - Future runAndExitOnError( - String executable, - List args, { - Directory workingDir, - }) async { - print( - 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); - final io.ProcessResult result = await io.Process.run(executable, args, - workingDirectory: workingDir?.path); - if (result.exitCode != 0) { - final String error = - _getErrorString(executable, args, workingDir: workingDir); - print('$error stdout:\n${result.stdout} \n stderr: ${result.stderr}'); - throw ToolExit(result.exitCode); - } - return result; - } - - String _getErrorString(String executable, List args, - {Directory workingDir}) { - final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; - return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; - } -} diff --git a/packages/flutter_plugin_tools/lib/src/create_all_plugins_app_command.dart b/packages/flutter_plugin_tools/lib/src/create_all_plugins_app_command.dart deleted file mode 100644 index 0f1431c5aee0..000000000000 --- a/packages/flutter_plugin_tools/lib/src/create_all_plugins_app_command.dart +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; - -import 'common.dart'; - -// TODO(cyanglaz): Add tests for this command. -// https://github.com/flutter/flutter/issues/61049 -class CreateAllPluginsAppCommand extends PluginCommand { - CreateAllPluginsAppCommand(Directory packagesDir, FileSystem fileSystem) - : super(packagesDir, fileSystem); - - @override - String get description => - 'Generate Flutter app that includes all plugins in packages.'; - - @override - String get name => 'all-plugins-app'; - - @override - Future run() async { - final int exitCode = await _createPlugin(); - if (exitCode != 0) { - throw ToolExit(exitCode); - } - - await Future.wait(>[ - _genPubspecWithAllPlugins(), - _updateAppGradle(), - _updateManifest(), - ]); - } - - Future _createPlugin() async { - final io.ProcessResult result = io.Process.runSync( - 'flutter', - [ - 'create', - '--template=app', - '--project-name=all_plugins', - '--android-language=java', - './all_plugins', - ], - ); - - print(result.stdout); - print(result.stderr); - return result.exitCode; - } - - Future _updateAppGradle() async { - final File gradleFile = fileSystem.file(p.join( - 'all_plugins', - 'android', - 'app', - 'build.gradle', - )); - if (!gradleFile.existsSync()) { - throw ToolExit(64); - } - - final StringBuffer newGradle = StringBuffer(); - for (String line in gradleFile.readAsLinesSync()) { - newGradle.writeln(line); - if (line.contains('defaultConfig {')) { - newGradle.writeln(' multiDexEnabled true'); - } else if (line.contains('dependencies {')) { - newGradle.writeln( - ' implementation \'com.google.guava:guava:27.0.1-android\'\n', - ); - // Tests for https://github.com/flutter/flutter/issues/43383 - newGradle.writeln( - " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", - ); - } - } - gradleFile.writeAsStringSync(newGradle.toString()); - } - - Future _updateManifest() async { - final File manifestFile = fileSystem.file(p.join( - 'all_plugins', - 'android', - 'app', - 'src', - 'main', - 'AndroidManifest.xml', - )); - if (!manifestFile.existsSync()) { - throw ToolExit(64); - } - - final StringBuffer newManifest = StringBuffer(); - for (String line in manifestFile.readAsLinesSync()) { - if (line.contains('package="com.example.all_plugins"')) { - newManifest - ..writeln('package="com.example.all_plugins"') - ..writeln('xmlns:tools="http://schemas.android.com/tools">') - ..writeln() - ..writeln( - '', - ); - } else { - newManifest.writeln(line); - } - } - manifestFile.writeAsStringSync(newManifest.toString()); - } - - Future _genPubspecWithAllPlugins() async { - final Map pluginDeps = - await _getValidPathDependencies(); - final Pubspec pubspec = Pubspec( - 'all_plugins', - description: 'Flutter app containing all 1st party plugins.', - version: Version.parse('1.0.0+1'), - environment: { - 'sdk': VersionConstraint.compatibleWith( - Version.parse('2.0.0'), - ), - }, - dependencies: { - 'flutter': SdkDependency('flutter'), - }..addAll(pluginDeps), - devDependencies: { - 'flutter_test': SdkDependency('flutter'), - }, - dependencyOverrides: pluginDeps, - ); - final File pubspecFile = - fileSystem.file(p.join('all_plugins', 'pubspec.yaml')); - pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); - } - - Future> _getValidPathDependencies() async { - final Map pathDependencies = - {}; - - await for (Directory package in getPlugins()) { - final String pluginName = package.path.split('/').last; - final File pubspecFile = - fileSystem.file(p.join(package.path, 'pubspec.yaml')); - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); - - if (pubspec.publishTo != 'none') { - pathDependencies[pluginName] = PathDependency(package.path); - } - } - return pathDependencies; - } - - String _pubspecToString(Pubspec pubspec) { - return ''' -### Generated file. Do not edit. Run `pub global run flutter_plugin_tools gen-pubspec` to update. -name: ${pubspec.name} -description: ${pubspec.description} - -version: ${pubspec.version} - -environment:${_pubspecMapString(pubspec.environment)} - -dependencies:${_pubspecMapString(pubspec.dependencies)} - -dependency_overrides:${_pubspecMapString(pubspec.dependencyOverrides)} - -dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} -###'''; - } - - String _pubspecMapString(Map values) { - final StringBuffer buffer = StringBuffer(); - - for (MapEntry entry in values.entries) { - buffer.writeln(); - if (entry.value is VersionConstraint) { - buffer.write(' ${entry.key}: ${entry.value}'); - } else if (entry.value is SdkDependency) { - final SdkDependency dep = entry.value; - buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); - } else if (entry.value is PathDependency) { - final PathDependency dep = entry.value; - buffer.write(' ${entry.key}: \n path: ${dep.path}'); - } else { - throw UnimplementedError( - 'Not available for type: ${entry.value.runtimeType}', - ); - } - } - - return buffer.toString(); - } -} diff --git a/packages/flutter_plugin_tools/lib/src/drive_examples_command.dart b/packages/flutter_plugin_tools/lib/src/drive_examples_command.dart deleted file mode 100644 index 8b1fa3624fbf..000000000000 --- a/packages/flutter_plugin_tools/lib/src/drive_examples_command.dart +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'common.dart'; - -class DriveExamplesCommand extends PluginCommand { - DriveExamplesCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addFlag(kLinux, - help: 'Runs the Linux implementation of the examples'); - argParser.addFlag(kMacos, - help: 'Runs the macOS implementation of the examples'); - argParser.addFlag(kWindows, - help: 'Runs the Windows implementation of the examples'); - argParser.addFlag(kIos, - help: 'Runs the iOS implementation of the examples'); - argParser.addFlag(kAndroid, - help: 'Runs the Android implementation of the examples'); - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: - 'Runs the driver tests in Dart VM with the given experiments enabled.', - ); - } - - @override - final String name = 'drive-examples'; - - @override - final String description = 'Runs driver tests for plugin example apps.\n\n' - 'For each *_test.dart in test_driver/ it drives an application with a ' - 'corresponding name in the test/ or test_driver/ directories.\n\n' - 'For example, test_driver/app_test.dart would match test/app.dart.\n\n' - 'This command requires "flutter" to be in your path.\n\n' - 'If a file with a corresponding name cannot be found, this driver file' - 'will be used to drive the tests that match ' - 'integration_test/*_test.dart.'; - - @override - Future run() async { - checkSharding(); - final List failingTests = []; - final bool isLinux = argResults[kLinux]; - final bool isMacos = argResults[kMacos]; - final bool isWindows = argResults[kWindows]; - await for (Directory plugin in getPlugins()) { - final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - for (Directory example in getExamplesForPlugin(plugin)) { - final String packageName = - p.relative(example.path, from: packagesDir.path); - if (!(await pluginSupportedOnCurrentPlatform(plugin, fileSystem))) { - continue; - } - final Directory driverTests = - fileSystem.directory(p.join(example.path, 'test_driver')); - if (!driverTests.existsSync()) { - // No driver tests available for this example - continue; - } - // Look for driver tests ending in _test.dart in test_driver/ - await for (FileSystemEntity test in driverTests.list()) { - final String driverTestName = - p.relative(test.path, from: driverTests.path); - if (!driverTestName.endsWith('_test.dart')) { - continue; - } - // Try to find a matching app to drive without the _test.dart - final String deviceTestName = driverTestName.replaceAll( - RegExp(r'_test.dart$'), - '.dart', - ); - String deviceTestPath = p.join('test', deviceTestName); - if (!fileSystem - .file(p.join(example.path, deviceTestPath)) - .existsSync()) { - // If the app isn't in test/ folder, look in test_driver/ instead. - deviceTestPath = p.join('test_driver', deviceTestName); - } - - final List targetPaths = []; - if (fileSystem - .file(p.join(example.path, deviceTestPath)) - .existsSync()) { - targetPaths.add(deviceTestPath); - } else { - final Directory integrationTests = - fileSystem.directory(p.join(example.path, 'integration_test')); - - if (await integrationTests.exists()) { - await for (FileSystemEntity integration_test - in integrationTests.list()) { - if (!integration_test.basename.endsWith('_test.dart')) { - continue; - } - targetPaths - .add(p.relative(integration_test.path, from: example.path)); - } - } - - if (targetPaths.isEmpty) { - print(''' -Unable to infer a target application for $driverTestName to drive. -Tried searching for the following: -1. test/$deviceTestName -2. test_driver/$deviceTestName -3. test_driver/*_test.dart -'''); - failingTests.add(p.relative(test.path, from: example.path)); - continue; - } - } - - final List driveArgs = ['drive', '-v']; - - final String enableExperiment = argResults[kEnableExperiment]; - if (enableExperiment.isNotEmpty) { - driveArgs.add('--enable-experiment=$enableExperiment'); - } - - if (isLinux && isLinuxPlugin(plugin, fileSystem)) { - driveArgs.addAll([ - '-d', - 'linux', - ]); - } - if (isMacos && isMacOsPlugin(plugin, fileSystem)) { - driveArgs.addAll([ - '-d', - 'macos', - ]); - } - if (isWindows && isWindowsPlugin(plugin, fileSystem)) { - driveArgs.addAll([ - '-d', - 'windows', - ]); - } - - for (final targetPath in targetPaths) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - ...driveArgs, - '--driver', - p.join('test_driver', driverTestName), - '--target', - targetPath, - ], - workingDir: example, - exitOnError: true); - if (exitCode != 0) { - failingTests.add(p.join(packageName, deviceTestPath)); - } - } - } - } - } - print('\n\n'); - - if (failingTests.isNotEmpty) { - print('The following driver tests are failing (see above for details):'); - for (String test in failingTests) { - print(' * $test'); - } - throw ToolExit(1); - } - - print('All driver tests successful!'); - } - - Future pluginSupportedOnCurrentPlatform( - FileSystemEntity plugin, FileSystem fileSystem) async { - final bool isLinux = argResults[kLinux]; - final bool isMacos = argResults[kMacos]; - final bool isWindows = argResults[kWindows]; - final bool isIOS = argResults[kIos]; - final bool isAndroid = argResults[kAndroid]; - if (isLinux) { - return isLinuxPlugin(plugin, fileSystem); - } - if (isMacos) { - return isMacOsPlugin(plugin, fileSystem); - } - if (isWindows) { - return isWindowsPlugin(plugin, fileSystem); - } - if (isIOS) { - return isIosPlugin(plugin, fileSystem); - } - if (isAndroid) { - return (isAndroidPlugin(plugin, fileSystem)); - } - // When we are here, no flags are specified. Only return true if the plugin supports mobile for legacy command support. - // TODO(cyanglaz): Make mobile platforms flags also required like other platforms (breaking change). - // https://github.com/flutter/flutter/issues/58285 - final bool isMobilePlugin = - isIosPlugin(plugin, fileSystem) || isAndroidPlugin(plugin, fileSystem); - return isMobilePlugin; - } -} diff --git a/packages/flutter_plugin_tools/lib/src/firebase_test_lab_command.dart b/packages/flutter_plugin_tools/lib/src/firebase_test_lab_command.dart deleted file mode 100644 index 0b4b2a471dbc..000000000000 --- a/packages/flutter_plugin_tools/lib/src/firebase_test_lab_command.dart +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:uuid/uuid.dart'; - -import 'common.dart'; - -class FirebaseTestLabCommand extends PluginCommand { - FirebaseTestLabCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - Print print = print, - }) : _print = print, - super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption( - 'project', - defaultsTo: 'flutter-infra', - help: 'The Firebase project name.', - ); - argParser.addOption('service-key', - defaultsTo: - p.join(io.Platform.environment['HOME'], 'gcloud-service-key.json')); - argParser.addOption('test-run-id', - defaultsTo: Uuid().v4(), - help: - 'Optional string to append to the results path, to avoid conflicts. ' - 'Randomly chosen on each invocation if none is provided. ' - 'The default shown here is just an example.'); - argParser.addMultiOption('device', - splitCommas: false, - defaultsTo: [ - 'model=walleye,version=26', - 'model=flame,version=29' - ], - help: - 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); - argParser.addOption('results-bucket', - defaultsTo: 'gs://flutter_firebase_testlab'); - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: 'Enables the given Dart SDK experiments.', - ); - } - - @override - final String name = 'firebase-test-lab'; - - @override - final String description = 'Runs the instrumentation tests of the example ' - 'apps on Firebase Test Lab.\n\n' - 'Runs tests in test_instrumentation folder using the ' - 'instrumentation_test package.'; - - static const String _gradleWrapper = 'gradlew'; - - final Print _print; - - Completer _firebaseProjectConfigured; - - Future _configureFirebaseProject() async { - if (_firebaseProjectConfigured != null) { - return _firebaseProjectConfigured.future; - } else { - _firebaseProjectConfigured = Completer(); - } - await processRunner.runAndExitOnError('gcloud', [ - 'auth', - 'activate-service-account', - '--key-file=${argResults['service-key']}', - ]); - int exitCode = await processRunner.runAndStream('gcloud', [ - 'config', - 'set', - 'project', - argResults['project'], - ]); - if (exitCode == 0) { - _print('\nFirebase project configured.'); - return; - } else { - _print( - '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.'); - } - _firebaseProjectConfigured.complete(null); - } - - @override - Future run() async { - checkSharding(); - final Stream packagesWithTests = getPackages().where( - (Directory d) => - isFlutterPackage(d, fileSystem) && - fileSystem - .directory(p.join( - d.path, 'example', 'android', 'app', 'src', 'androidTest')) - .existsSync()); - - final List failingPackages = []; - final List missingFlutterBuild = []; - int resultsCounter = - 0; // We use a unique GCS bucket for each Firebase Test Lab run - await for (Directory package in packagesWithTests) { - // See https://github.com/flutter/flutter/issues/38983 - - final Directory exampleDirectory = - fileSystem.directory(p.join(package.path, 'example')); - final String packageName = - p.relative(package.path, from: packagesDir.path); - _print('\nRUNNING FIREBASE TEST LAB TESTS for $packageName'); - - final Directory androidDirectory = - fileSystem.directory(p.join(exampleDirectory.path, 'android')); - - final String enableExperiment = argResults[kEnableExperiment]; - final String encodedEnableExperiment = - Uri.encodeComponent('--enable-experiment=$enableExperiment'); - - // Ensures that gradle wrapper exists - if (!fileSystem - .file(p.join(androidDirectory.path, _gradleWrapper)) - .existsSync()) { - final int exitCode = await processRunner.runAndStream( - 'flutter', - [ - 'build', - 'apk', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: androidDirectory); - - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - continue; - } - - await _configureFirebaseProject(); - - int exitCode = await processRunner.runAndStream( - p.join(androidDirectory.path, _gradleWrapper), - [ - 'app:assembleAndroidTest', - '-Pverbose=true', - if (enableExperiment.isNotEmpty) - '-Pextra-front-end-options=$encodedEnableExperiment', - if (enableExperiment.isNotEmpty) - '-Pextra-gen-snapshot-options=$encodedEnableExperiment', - ], - workingDir: androidDirectory); - - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - - // Look for tests recursively in folders that start with 'test' and that - // live in the root or example folders. - bool isTestDir(FileSystemEntity dir) { - return p.basename(dir.path).startsWith('test') || - p.basename(dir.path) == 'integration_test'; - } - - final List testDirs = - package.listSync().where(isTestDir).toList(); - final Directory example = - fileSystem.directory(p.join(package.path, 'example')); - testDirs.addAll(example.listSync().where(isTestDir).toList()); - for (Directory testDir in testDirs) { - bool isE2ETest(FileSystemEntity file) { - return file.path.endsWith('_e2e.dart') || - (file.parent.basename == 'integration_test' && - file.path.endsWith('_test.dart')); - } - - final List testFiles = testDir - .listSync(recursive: true, followLinks: true) - .where(isE2ETest) - .toList(); - for (FileSystemEntity test in testFiles) { - exitCode = await processRunner.runAndStream( - p.join(androidDirectory.path, _gradleWrapper), - [ - 'app:assembleDebug', - '-Pverbose=true', - '-Ptarget=${test.path}', - if (enableExperiment.isNotEmpty) - '-Pextra-front-end-options=$encodedEnableExperiment', - if (enableExperiment.isNotEmpty) - '-Pextra-gen-snapshot-options=$encodedEnableExperiment', - ], - workingDir: androidDirectory); - - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - final String buildId = io.Platform.environment['CIRRUS_BUILD_ID']; - final String testRunId = argResults['test-run-id']; - final String resultsDir = - 'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/'; - final List args = [ - 'firebase', - 'test', - 'android', - 'run', - '--type', - 'instrumentation', - '--app', - 'build/app/outputs/apk/debug/app-debug.apk', - '--test', - 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', - '--timeout', - '5m', - '--results-bucket=${argResults['results-bucket']}', - '--results-dir=${resultsDir}', - ]; - for (String device in argResults['device']) { - args.addAll(['--device', device]); - } - exitCode = await processRunner.runAndStream('gcloud', args, - workingDir: exampleDirectory); - - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - } - } - } - - _print('\n\n'); - if (failingPackages.isNotEmpty) { - _print( - 'The instrumentation tests for the following packages are failing (see above for' - 'details):'); - for (String package in failingPackages) { - _print(' * $package'); - } - } - if (missingFlutterBuild.isNotEmpty) { - _print('Run "pub global run flutter_plugin_tools build-examples --apk" on' - 'the following packages before executing tests again:'); - for (String package in missingFlutterBuild) { - _print(' * $package'); - } - } - - if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { - throw ToolExit(1); - } - - _print('All Firebase Test Lab tests successful!'); - } -} diff --git a/packages/flutter_plugin_tools/lib/src/format_command.dart b/packages/flutter_plugin_tools/lib/src/format_command.dart deleted file mode 100644 index ec326b96c1f9..000000000000 --- a/packages/flutter_plugin_tools/lib/src/format_command.dart +++ /dev/null @@ -1,147 +0,0 @@ -// 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 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as p; -import 'package:quiver/iterables.dart'; - -import 'common.dart'; - -const String _googleFormatterUrl = - 'https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'; - -class FormatCommand extends PluginCommand { - FormatCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addFlag('travis', hide: true); - argParser.addOption('clang-format', - defaultsTo: 'clang-format', - help: 'Path to executable of clang-format v5.'); - } - - @override - final String name = 'format'; - - @override - final String description = - 'Formats the code of all packages (Java, Objective-C, C++, and Dart).\n\n' - 'This command requires "git", "flutter" and "clang-format" v5 to be in ' - 'your path.'; - - @override - Future run() async { - checkSharding(); - final String googleFormatterPath = await _getGoogleFormatterPath(); - - await _formatDart(); - await _formatJava(googleFormatterPath); - await _formatCppAndObjectiveC(); - - if (argResults['travis']) { - final bool modified = await _didModifyAnything(); - if (modified) { - throw ToolExit(1); - } - } - } - - Future _didModifyAnything() async { - final io.ProcessResult modifiedFiles = await processRunner - .runAndExitOnError('git', ['ls-files', '--modified'], - workingDir: packagesDir); - - print('\n\n'); - - if (modifiedFiles.stdout.isEmpty) { - print('All files formatted correctly.'); - return false; - } - - print('These files are not formatted correctly (see diff below):'); - LineSplitter.split(modifiedFiles.stdout) - .map((String line) => ' $line') - .forEach(print); - - print('\nTo fix run "pub global activate flutter_plugin_tools && ' - 'pub global run flutter_plugin_tools format" or copy-paste ' - 'this command into your terminal:'); - - print('patch -p1 <['diff'], workingDir: packagesDir); - print(diff.stdout); - print('DONE'); - return true; - } - - Future _formatCppAndObjectiveC() async { - print('Formatting all .cc, .cpp, .mm, .m, and .h files...'); - final Iterable allFiles = [] - ..addAll(await _getFilesWithExtension('.h')) - ..addAll(await _getFilesWithExtension('.m')) - ..addAll(await _getFilesWithExtension('.mm')) - ..addAll(await _getFilesWithExtension('.cc')) - ..addAll(await _getFilesWithExtension('.cpp')); - // Split this into multiple invocations to avoid a - // 'ProcessException: Argument list too long'. - final Iterable> batches = partition(allFiles, 100); - for (List batch in batches) { - await processRunner.runAndStream(argResults['clang-format'], - ['-i', '--style=Google']..addAll(batch), - workingDir: packagesDir, exitOnError: true); - } - } - - Future _formatJava(String googleFormatterPath) async { - print('Formatting all .java files...'); - final Iterable javaFiles = await _getFilesWithExtension('.java'); - await processRunner.runAndStream('java', - ['-jar', googleFormatterPath, '--replace']..addAll(javaFiles), - workingDir: packagesDir, exitOnError: true); - } - - Future _formatDart() async { - // This actually should be fine for non-Flutter Dart projects, no need to - // specifically shell out to dartfmt -w in that case. - print('Formatting all .dart files...'); - final Iterable dartFiles = await _getFilesWithExtension('.dart'); - if (dartFiles.isEmpty) { - print( - 'No .dart files to format. If you set the `--exclude` flag, most likey they were skipped'); - } else { - await processRunner.runAndStream( - 'flutter', ['format']..addAll(dartFiles), - workingDir: packagesDir, exitOnError: true); - } - } - - Future> _getFilesWithExtension(String extension) async => - getFiles() - .where((File file) => p.extension(file.path) == extension) - .map((File file) => file.path) - .toList(); - - Future _getGoogleFormatterPath() async { - final String javaFormatterPath = p.join( - p.dirname(p.fromUri(io.Platform.script)), - 'google-java-format-1.3-all-deps.jar'); - final File javaFormatterFile = fileSystem.file(javaFormatterPath); - - if (!javaFormatterFile.existsSync()) { - print('Downloading Google Java Format...'); - final http.Response response = await http.get(_googleFormatterUrl); - javaFormatterFile.writeAsBytesSync(response.bodyBytes); - } - - return javaFormatterPath; - } -} diff --git a/packages/flutter_plugin_tools/lib/src/java_test_command.dart b/packages/flutter_plugin_tools/lib/src/java_test_command.dart deleted file mode 100644 index cf605bfc5ce2..000000000000 --- a/packages/flutter_plugin_tools/lib/src/java_test_command.dart +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; - -import 'common.dart'; - -class JavaTestCommand extends PluginCommand { - JavaTestCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner); - - @override - final String name = 'java-test'; - - @override - final String description = 'Runs the Java tests of the example apps.\n\n' - 'Building the apks of the example apps is required before executing this' - 'command.'; - - static const String _gradleWrapper = 'gradlew'; - - @override - Future run() async { - checkSharding(); - final Stream examplesWithTests = getExamples().where( - (Directory d) => - isFlutterPackage(d, fileSystem) && - fileSystem - .directory(p.join(d.path, 'android', 'app', 'src', 'test')) - .existsSync()); - - final List failingPackages = []; - final List missingFlutterBuild = []; - await for (Directory example in examplesWithTests) { - final String packageName = - p.relative(example.path, from: packagesDir.path); - print('\nRUNNING JAVA TESTS for $packageName'); - - final Directory androidDirectory = - fileSystem.directory(p.join(example.path, 'android')); - if (!fileSystem - .file(p.join(androidDirectory.path, _gradleWrapper)) - .existsSync()) { - print('ERROR: Run "flutter build apk" on example app of $packageName' - 'before executing tests.'); - missingFlutterBuild.add(packageName); - continue; - } - - final int exitCode = await processRunner.runAndStream( - p.join(androidDirectory.path, _gradleWrapper), - ['testDebugUnitTest', '--info'], - workingDir: androidDirectory); - if (exitCode != 0) { - failingPackages.add(packageName); - } - } - - print('\n\n'); - if (failingPackages.isNotEmpty) { - print( - 'The Java tests for the following packages are failing (see above for' - 'details):'); - for (String package in failingPackages) { - print(' * $package'); - } - } - if (missingFlutterBuild.isNotEmpty) { - print('Run "pub global run flutter_plugin_tools build-examples --apk" on' - 'the following packages before executing tests again:'); - for (String package in missingFlutterBuild) { - print(' * $package'); - } - } - - if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { - throw ToolExit(1); - } - - print('All Java tests successful!'); - } -} diff --git a/packages/flutter_plugin_tools/lib/src/lint_podspecs_command.dart b/packages/flutter_plugin_tools/lib/src/lint_podspecs_command.dart deleted file mode 100644 index 68fd4b61dd66..000000000000 --- a/packages/flutter_plugin_tools/lib/src/lint_podspecs_command.dart +++ /dev/null @@ -1,146 +0,0 @@ -// 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 'dart:convert'; -import 'dart:io'; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; - -import 'common.dart'; - -typedef void Print(Object object); - -/// Lint the CocoaPod podspecs, run the static analyzer on iOS/macOS plugin -/// platform code, and run unit tests. -/// -/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. -class LintPodspecsCommand extends PluginCommand { - LintPodspecsCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - this.platform = const LocalPlatform(), - Print print = print, - }) : _print = print, - super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addMultiOption('skip', - help: - 'Skip all linting for podspecs with this basename (example: federated plugins with placeholder podspecs)', - valueHelp: 'podspec_file_name'); - argParser.addMultiOption('ignore-warnings', - help: - 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs with this basename (example: plugins with known warnings)', - valueHelp: 'podspec_file_name'); - argParser.addMultiOption('no-analyze', - help: - 'Do not pass --analyze flag to "pod lib lint" for podspecs with this basename (example: plugins with known analyzer warnings)', - valueHelp: 'podspec_file_name'); - } - - @override - final String name = 'podspecs'; - - @override - List get aliases => ['podspec']; - - @override - final String description = - 'Runs "pod lib lint" on all iOS and macOS plugin podspecs.\n\n' - 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; - - final Platform platform; - - final Print _print; - - @override - Future run() async { - if (!platform.isMacOS) { - _print('Detected platform is not macOS, skipping podspec lint'); - return; - } - - checkSharding(); - - await processRunner.runAndExitOnError('which', ['pod'], - workingDir: packagesDir); - - _print('Starting podspec lint test'); - - final List failingPlugins = []; - for (File podspec in await _podspecsToLint()) { - if (!await _lintPodspec(podspec)) { - failingPlugins.add(p.basenameWithoutExtension(podspec.path)); - } - } - - _print('\n\n'); - if (failingPlugins.isNotEmpty) { - _print('The following plugins have podspec errors (see above):'); - failingPlugins.forEach((String plugin) { - _print(' * $plugin'); - }); - throw ToolExit(1); - } - } - - Future> _podspecsToLint() async { - final List podspecs = await getFiles().where((File entity) { - final String filePath = entity.path; - return p.extension(filePath) == '.podspec' && - !argResults['skip'].contains(p.basenameWithoutExtension(filePath)); - }).toList(); - - podspecs.sort( - (File a, File b) => p.basename(a.path).compareTo(p.basename(b.path))); - return podspecs; - } - - Future _lintPodspec(File podspec) async { - // Do not run the static analyzer on plugins with known analyzer issues. - final String podspecPath = podspec.path; - final bool runAnalyzer = !argResults['no-analyze'] - .contains(p.basenameWithoutExtension(podspecPath)); - - final String podspecBasename = p.basename(podspecPath); - if (runAnalyzer) { - _print('Linting and analyzing $podspecBasename'); - } else { - _print('Linting $podspecBasename'); - } - - // Lint plugin as framework (use_frameworks!). - final ProcessResult frameworkResult = await _runPodLint(podspecPath, - runAnalyzer: runAnalyzer, libraryLint: true); - _print(frameworkResult.stdout); - _print(frameworkResult.stderr); - - // Lint plugin as library. - final ProcessResult libraryResult = await _runPodLint(podspecPath, - runAnalyzer: runAnalyzer, libraryLint: false); - _print(libraryResult.stdout); - _print(libraryResult.stderr); - - return frameworkResult.exitCode == 0 && libraryResult.exitCode == 0; - } - - Future _runPodLint(String podspecPath, - {bool runAnalyzer, bool libraryLint}) async { - final bool allowWarnings = argResults['ignore-warnings'] - .contains(p.basenameWithoutExtension(podspecPath)); - final List arguments = [ - 'lib', - 'lint', - podspecPath, - if (allowWarnings) '--allow-warnings', - if (runAnalyzer) '--analyze', - if (libraryLint) '--use-libraries' - ]; - - return processRunner.run('pod', arguments, - workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); - } -} diff --git a/packages/flutter_plugin_tools/lib/src/list_command.dart b/packages/flutter_plugin_tools/lib/src/list_command.dart deleted file mode 100644 index 7f94daac7096..000000000000 --- a/packages/flutter_plugin_tools/lib/src/list_command.dart +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:file/file.dart'; - -import 'common.dart'; - -class ListCommand extends PluginCommand { - ListCommand(Directory packagesDir, FileSystem fileSystem) - : super(packagesDir, fileSystem) { - argParser.addOption( - _type, - defaultsTo: _plugin, - allowed: [_plugin, _example, _package, _file], - help: 'What type of file system content to list.', - ); - } - - static const String _type = 'type'; - static const String _plugin = 'plugin'; - static const String _example = 'example'; - static const String _package = 'package'; - static const String _file = 'file'; - - @override - final String name = 'list'; - - @override - final String description = 'Lists packages or files'; - - @override - Future run() async { - checkSharding(); - switch (argResults[_type]) { - case _plugin: - await for (Directory package in getPlugins()) { - print(package.path); - } - break; - case _example: - await for (Directory package in getExamples()) { - print(package.path); - } - break; - case _package: - await for (Directory package in getPackages()) { - print(package.path); - } - break; - case _file: - await for (File file in getFiles()) { - print(file.path); - } - break; - } - } -} diff --git a/packages/flutter_plugin_tools/lib/src/main.dart b/packages/flutter_plugin_tools/lib/src/main.dart deleted file mode 100644 index bb3f67c0a9e1..000000000000 --- a/packages/flutter_plugin_tools/lib/src/main.dart +++ /dev/null @@ -1,63 +0,0 @@ -// 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:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; -import 'package:path/path.dart' as p; - -import 'analyze_command.dart'; -import 'build_examples_command.dart'; -import 'common.dart'; -import 'create_all_plugins_app_command.dart'; -import 'drive_examples_command.dart'; -import 'firebase_test_lab_command.dart'; -import 'format_command.dart'; -import 'java_test_command.dart'; -import 'lint_podspecs_command.dart'; -import 'list_command.dart'; -import 'test_command.dart'; -import 'version_check_command.dart'; -import 'xctest_command.dart'; - -void main(List args) { - final FileSystem fileSystem = const LocalFileSystem(); - - Directory packagesDir = fileSystem - .directory(p.join(fileSystem.currentDirectory.path, 'packages')); - - if (!packagesDir.existsSync()) { - if (p.basename(fileSystem.currentDirectory.path) == 'packages') { - packagesDir = fileSystem.currentDirectory; - } else { - print('Error: Cannot find a "packages" sub-directory'); - io.exit(1); - } - } - - final CommandRunner commandRunner = CommandRunner( - 'pub global run flutter_plugin_tools', - 'Productivity utils for hosting multiple plugins within one repository.') - ..addCommand(AnalyzeCommand(packagesDir, fileSystem)) - ..addCommand(BuildExamplesCommand(packagesDir, fileSystem)) - ..addCommand(CreateAllPluginsAppCommand(packagesDir, fileSystem)) - ..addCommand(DriveExamplesCommand(packagesDir, fileSystem)) - ..addCommand(FirebaseTestLabCommand(packagesDir, fileSystem)) - ..addCommand(FormatCommand(packagesDir, fileSystem)) - ..addCommand(JavaTestCommand(packagesDir, fileSystem)) - ..addCommand(LintPodspecsCommand(packagesDir, fileSystem)) - ..addCommand(ListCommand(packagesDir, fileSystem)) - ..addCommand(PublishPluginCommand(packagesDir, fileSystem)) - ..addCommand(TestCommand(packagesDir, fileSystem)) - ..addCommand(VersionCheckCommand(packagesDir, fileSystem)) - ..addCommand(XCTestCommand(packagesDir, fileSystem)); - - commandRunner.run(args).catchError((Object e) { - final ToolExit toolExit = e; - io.exit(toolExit.exitCode); - }, test: (Object e) => e is ToolExit); -} diff --git a/packages/flutter_plugin_tools/lib/src/publish_plugin_command.dart b/packages/flutter_plugin_tools/lib/src/publish_plugin_command.dart deleted file mode 100644 index 55c48f5484e5..000000000000 --- a/packages/flutter_plugin_tools/lib/src/publish_plugin_command.dart +++ /dev/null @@ -1,227 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; -import 'package:yaml/yaml.dart'; - -import 'common.dart'; - -/// Wraps pub publish with a few niceties used by the flutter/plugin team. -/// -/// 1. Checks for any modified files in git and refuses to publish if there's an -/// issue. -/// 2. Tags the release with the format -v. -/// 3. Pushes the release to a remote. -/// -/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full -/// usage information. -/// -/// [processRunner], [print], and [stdin] can be overriden for easier testing. -class PublishPluginCommand extends PluginCommand { - PublishPluginCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - Print print = print, - Stdin stdinput, - }) : _print = print, - _stdin = stdinput ?? stdin, - super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption( - _packageOption, - help: 'The package to publish.' - 'If the package directory name is different than its pubspec.yaml name, then this should specify the directory.', - ); - argParser.addMultiOption(_pubFlagsOption, - help: - 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); - argParser.addFlag( - _tagReleaseOption, - help: 'Whether or not to tag the release.', - defaultsTo: true, - negatable: true, - ); - argParser.addFlag( - _pushTagsOption, - help: - 'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.', - defaultsTo: true, - negatable: true, - ); - argParser.addOption( - _remoteOption, - help: - 'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.', - // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. - defaultsTo: 'upstream', - ); - } - - static const String _packageOption = 'package'; - static const String _tagReleaseOption = 'tag-release'; - static const String _pushTagsOption = 'push-tags'; - static const String _pubFlagsOption = 'pub-publish-flags'; - static const String _remoteOption = 'remote'; - - // Version tags should follow -v. For example, - // `flutter_plugin_tools-v0.0.24`. - static const String _tagFormat = '%PACKAGE%-v%VERSION%'; - - @override - final String name = 'publish-plugin'; - - @override - final String description = - 'Attempts to publish the given plugin and tag its release on GitHub.'; - - final Print _print; - final Stdin _stdin; - // The directory of the actual package that we are publishing. - Directory _packageDir; - StreamSubscription _stdinSubscription; - - @override - Future run() async { - checkSharding(); - _print('Checking local repo...'); - _packageDir = _checkPackageDir(); - await _checkGitStatus(); - final bool shouldPushTag = argResults[_pushTagsOption]; - final String remote = argResults[_remoteOption]; - String remoteUrl; - if (shouldPushTag) { - remoteUrl = await _verifyRemote(remote); - } - _print('Local repo is ready!'); - - await _publish(); - _print('Package published!'); - if (!argResults[_tagReleaseOption]) { - return await _finishSuccesfully(); - } - - _print('Tagging release...'); - final String tag = _getTag(); - await processRunner.runAndExitOnError('git', ['tag', tag], - workingDir: _packageDir); - if (!shouldPushTag) { - return await _finishSuccesfully(); - } - - _print('Pushing tag to $remote...'); - await _pushTagToRemote(remote: remote, tag: tag, remoteUrl: remoteUrl); - await _finishSuccesfully(); - } - - Future _finishSuccesfully() async { - await _stdinSubscription.cancel(); - _print('Done!'); - } - - Directory _checkPackageDir() { - final String package = argResults[_packageOption]; - if (package == null) { - _print( - 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); - throw ToolExit(1); - } - final Directory _packageDir = packagesDir.childDirectory(package); - if (!_packageDir.existsSync()) { - _print('${_packageDir.absolute.path} does not exist.'); - throw ToolExit(1); - } - if (!isFlutterPackage(_packageDir, fileSystem)) { - _print('${_packageDir.absolute.path} is not a flutter package.'); - throw ToolExit(1); - } - return _packageDir; - } - - Future _checkGitStatus() async { - if (!await GitDir.isGitDir(packagesDir.path)) { - _print('$packagesDir is not a valid Git repository.'); - throw ToolExit(1); - } - - final ProcessResult statusResult = await processRunner.runAndExitOnError( - 'git', - [ - 'status', - '--porcelain', - '--ignored', - _packageDir.absolute.path - ], - workingDir: _packageDir); - final String statusOutput = statusResult.stdout; - if (statusOutput.isNotEmpty) { - _print( - "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" - '$statusOutput\n' - 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); - throw ToolExit(1); - } - } - - Future _verifyRemote(String remote) async { - final ProcessResult remoteInfo = await processRunner.runAndExitOnError( - 'git', ['remote', 'get-url', remote], - workingDir: _packageDir); - return remoteInfo.stdout; - } - - Future _publish() async { - final List publishFlags = argResults[_pubFlagsOption]; - _print( - 'Running `pub publish ${publishFlags.join(' ')}` in ${_packageDir.absolute.path}...\n'); - final Process publish = await processRunner.start( - 'flutter', ['pub', 'publish'] + publishFlags, - workingDirectory: _packageDir); - publish.stdout - .transform(utf8.decoder) - .listen((String data) => _print(data)); - publish.stderr - .transform(utf8.decoder) - .listen((String data) => _print(data)); - _stdinSubscription = _stdin - .transform(utf8.decoder) - .listen((String data) => publish.stdin.writeln(data)); - final int result = await publish.exitCode; - if (result != 0) { - _print('Publish failed. Exiting.'); - throw ToolExit(result); - } - } - - String _getTag() { - final File pubspecFile = - fileSystem.file(p.join(_packageDir.path, 'pubspec.yaml')); - final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); - final String name = pubspecYaml['name']; - final String version = pubspecYaml['version']; - // We should have failed to publish if these were unset. - assert(name.isNotEmpty && version.isNotEmpty); - return _tagFormat - .replaceAll('%PACKAGE%', name) - .replaceAll('%VERSION%', version); - } - - Future _pushTagToRemote( - {@required String remote, - @required String tag, - @required String remoteUrl}) async { - assert(remote != null && tag != null && remoteUrl != null); - _print('Ready to push $tag to $remoteUrl (y/n)?'); - final String input = _stdin.readLineSync(); - if (input.toLowerCase() != 'y') { - _print('Tag push canceled.'); - throw ToolExit(1); - } - - await processRunner.runAndExitOnError('git', ['push', remote, tag], - workingDir: packagesDir); - } -} diff --git a/packages/flutter_plugin_tools/lib/src/test_command.dart b/packages/flutter_plugin_tools/lib/src/test_command.dart deleted file mode 100644 index e938168cfa89..000000000000 --- a/packages/flutter_plugin_tools/lib/src/test_command.dart +++ /dev/null @@ -1,101 +0,0 @@ -// 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:file/file.dart'; -import 'package:path/path.dart' as p; - -import 'common.dart'; - -class TestCommand extends PluginCommand { - TestCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: 'Runs the tests in Dart VM with the given experiments enabled.', - ); - } - - @override - final String name = 'test'; - - @override - final String description = 'Runs the Dart tests for all packages.\n\n' - 'This command requires "flutter" to be in your path.'; - - @override - Future run() async { - checkSharding(); - final List failingPackages = []; - await for (Directory packageDir in getPackages()) { - final String packageName = - p.relative(packageDir.path, from: packagesDir.path); - if (!fileSystem.directory(p.join(packageDir.path, 'test')).existsSync()) { - print('SKIPPING $packageName - no test subdirectory'); - continue; - } - - print('RUNNING $packageName tests...'); - - final String enableExperiment = argResults[kEnableExperiment]; - - // `flutter test` automatically gets packages. `pub run test` does not. :( - int exitCode = 0; - if (isFlutterPackage(packageDir, fileSystem)) { - final List args = [ - 'test', - '--color', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ]; - - if (isWebPlugin(packageDir, fileSystem)) { - args.add('--platform=chrome'); - } - exitCode = await processRunner.runAndStream( - 'flutter', - args, - workingDir: packageDir, - ); - } else { - exitCode = await processRunner.runAndStream( - 'pub', - ['get'], - workingDir: packageDir, - ); - if (exitCode == 0) { - exitCode = await processRunner.runAndStream( - 'pub', - [ - 'run', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - 'test', - ], - workingDir: packageDir, - ); - } - } - if (exitCode != 0) { - failingPackages.add(packageName); - } - } - - print('\n\n'); - if (failingPackages.isNotEmpty) { - print('Tests for the following packages are failing (see above):'); - failingPackages.forEach((String package) { - print(' * $package'); - }); - throw ToolExit(1); - } - - print('All tests are passing!'); - } -} diff --git a/packages/flutter_plugin_tools/lib/src/version_check_command.dart b/packages/flutter_plugin_tools/lib/src/version_check_command.dart deleted file mode 100644 index 2c6b92bbcb7a..000000000000 --- a/packages/flutter_plugin_tools/lib/src/version_check_command.dart +++ /dev/null @@ -1,220 +0,0 @@ -// 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 'dart:io' as io; - -import 'package:meta/meta.dart'; -import 'package:colorize/colorize.dart'; -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; -import 'package:yaml/yaml.dart'; - -import 'common.dart'; - -const String _kBaseSha = 'base_sha'; - -class GitVersionFinder { - GitVersionFinder(this.baseGitDir, this.baseSha); - - final GitDir baseGitDir; - final String baseSha; - - static bool isPubspec(String file) { - return file.trim().endsWith('pubspec.yaml'); - } - - Future> getChangedPubSpecs() async { - final io.ProcessResult changedFilesCommand = await baseGitDir - .runCommand(['diff', '--name-only', '$baseSha', 'HEAD']); - final List changedFiles = - changedFilesCommand.stdout.toString().split('\n'); - return changedFiles.where(isPubspec).toList(); - } - - Future getPackageVersion(String pubspecPath, String gitRef) async { - final io.ProcessResult gitShow = - await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); - final String fileContent = gitShow.stdout; - final String versionString = loadYaml(fileContent)['version']; - return versionString == null ? null : Version.parse(versionString); - } -} - -enum NextVersionType { - BREAKING_MAJOR, - MAJOR_NULLSAFETY_PRE_RELEASE, - MINOR_NULLSAFETY_PRE_RELEASE, - MINOR, - PATCH, - RELEASE, -} - -Version getNextNullSafetyPreRelease(Version current, Version next) { - String nextNullsafetyPrerelease = 'nullsafety'; - if (current.isPreRelease && - current.preRelease.first is String && - current.preRelease.first == 'nullsafety') { - if (current.preRelease.length == 1) { - nextNullsafetyPrerelease = 'nullsafety.1'; - } else if (current.preRelease.length == 2 && - current.preRelease.last is int) { - nextNullsafetyPrerelease = 'nullsafety.${current.preRelease.last + 1}'; - } - } - return Version( - next.major, - next.minor, - next.patch, - pre: nextNullsafetyPrerelease, - ); -} - -@visibleForTesting -Map getAllowedNextVersions( - Version masterVersion, Version headVersion) { - final Version nextNullSafetyMajor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMajor); - final Version nextNullSafetyMinor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); - final Map allowedNextVersions = - { - masterVersion.nextMajor: NextVersionType.BREAKING_MAJOR, - nextNullSafetyMajor: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE, - nextNullSafetyMinor: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE, - masterVersion.nextMinor: NextVersionType.MINOR, - masterVersion.nextPatch: NextVersionType.PATCH, - }; - - if (masterVersion.major < 1 && headVersion.major < 1) { - int nextBuildNumber = -1; - if (masterVersion.build.isEmpty) { - nextBuildNumber = 1; - } else { - final int currentBuildNumber = masterVersion.build.first; - nextBuildNumber = currentBuildNumber + 1; - } - final Version preReleaseVersion = Version( - masterVersion.major, - masterVersion.minor, - masterVersion.patch, - build: nextBuildNumber.toString(), - ); - allowedNextVersions.clear(); - allowedNextVersions[masterVersion.nextMajor] = NextVersionType.RELEASE; - allowedNextVersions[masterVersion.nextMinor] = - NextVersionType.BREAKING_MAJOR; - allowedNextVersions[masterVersion.nextPatch] = NextVersionType.MINOR; - allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; - - final Version nextNullSafetyMajor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); - final Version nextNullSafetyMinor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextPatch); - - allowedNextVersions[nextNullSafetyMajor] = - NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE; - allowedNextVersions[nextNullSafetyMinor] = - NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE; - } - return allowedNextVersions; -} - -class VersionCheckCommand extends PluginCommand { - VersionCheckCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - this.gitDir, - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption(_kBaseSha); - } - - /// The git directory to use. By default it uses the parent directory. - /// - /// This can be mocked for testing. - final GitDir gitDir; - - @override - final String name = 'version-check'; - - @override - final String description = - 'Checks if the versions of the plugins have been incremented per pub specification.\n\n' - 'This command requires "pub" and "flutter" to be in your path.'; - - @override - Future run() async { - checkSharding(); - - final String rootDir = packagesDir.parent.absolute.path; - final String baseSha = argResults[_kBaseSha]; - - GitDir baseGitDir = gitDir; - if (baseGitDir == null) { - if (!await GitDir.isGitDir(rootDir)) { - print('$rootDir is not a valid Git repository.'); - throw ToolExit(2); - } - baseGitDir = await GitDir.fromExisting(rootDir); - } - - final GitVersionFinder gitVersionFinder = - GitVersionFinder(baseGitDir, baseSha); - - final List changedPubspecs = - await gitVersionFinder.getChangedPubSpecs(); - - for (final String pubspecPath in changedPubspecs) { - try { - final File pubspecFile = fileSystem.file(pubspecPath); - if (!pubspecFile.existsSync()) { - continue; - } - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); - if (pubspec.publishTo == 'none') { - continue; - } - - final Version masterVersion = - await gitVersionFinder.getPackageVersion(pubspecPath, baseSha); - final Version headVersion = - await gitVersionFinder.getPackageVersion(pubspecPath, 'HEAD'); - if (headVersion == null) { - continue; // Example apps don't have versions - } - - final Map allowedNextVersions = - getAllowedNextVersions(masterVersion, headVersion); - - if (!allowedNextVersions.containsKey(headVersion)) { - final String error = '$pubspecPath incorrectly updated version.\n' - 'HEAD: $headVersion, master: $masterVersion.\n' - 'Allowed versions: $allowedNextVersions'; - final Colorize redError = Colorize(error)..red(); - print(redError); - throw ToolExit(1); - } - - bool isPlatformInterface = pubspec.name.endsWith("_platform_interface"); - if (isPlatformInterface && - allowedNextVersions[headVersion] == - NextVersionType.BREAKING_MAJOR) { - final String error = '$pubspecPath breaking change detected.\n' - 'Breaking changes to platform interfaces are strongly discouraged.\n'; - final Colorize redError = Colorize(error)..red(); - print(redError); - throw ToolExit(1); - } - } on io.ProcessException { - print('Unable to find pubspec in master for $pubspecPath.' - ' Safe to ignore if the project is new.'); - } - } - - print('No version check errors found!'); - } -} diff --git a/packages/flutter_plugin_tools/lib/src/xctest_command.dart b/packages/flutter_plugin_tools/lib/src/xctest_command.dart deleted file mode 100644 index d90b7a8fbfea..000000000000 --- a/packages/flutter_plugin_tools/lib/src/xctest_command.dart +++ /dev/null @@ -1,216 +0,0 @@ -// 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 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; - -import 'common.dart'; - -const String _kiOSDestination = 'ios-destination'; -const String _kTarget = 'target'; -const String _kSkip = 'skip'; -const String _kXcodeBuildCommand = 'xcodebuild'; -const String _kXCRunCommand = 'xcrun'; -const String _kFoundNoSimulatorsMessage = - 'Cannot find any available simulators, tests failed'; - -/// The command to run iOS' XCTests in plugins, this should work for both XCUnitTest and XCUITest targets. -/// The tests target have to be added to the xcode project of the example app. Usually at "example/ios/Runner.xcodeproj". -/// The command takes a "-target" argument which has to match the target of the test target. -/// For information on how to add test target in an xcode project, see https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/UnitTesting.html -class XCTestCommand extends PluginCommand { - XCTestCommand( - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption( - _kiOSDestination, - help: - 'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n' - 'this is passed to the `-destination` argument in xcodebuild command.\n' - 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', - ); - argParser.addOption(_kTarget, - help: 'The test target.\n' - 'This is the xcode project test target. This is passed to the `-scheme` argument in the xcodebuild command. \n' - 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the scheme'); - argParser.addMultiOption(_kSkip, - help: 'Plugins to skip while running this command. \n'); - } - - @override - final String name = 'xctest'; - - @override - final String description = 'Runs the xctests in the iOS example apps.\n\n' - 'This command requires "flutter" to be in your path.'; - - @override - Future run() async { - if (argResults[_kTarget] == null) { - // TODO(cyanglaz): Automatically find all the available testing schemes if this argument is not specified. - // https://github.com/flutter/flutter/issues/68419 - print('--$_kTarget must be specified'); - throw ToolExit(1); - } - - String destination = argResults[_kiOSDestination]; - if (destination == null) { - String simulatorId = await _findAvailableIphoneSimulator(); - if (simulatorId == null) { - print(_kFoundNoSimulatorsMessage); - throw ToolExit(1); - } - destination = 'id=$simulatorId'; - } - - checkSharding(); - - final String target = argResults[_kTarget]; - final List skipped = argResults[_kSkip]; - - List failingPackages = []; - await for (Directory plugin in getPlugins()) { - // Start running for package. - final String packageName = - p.relative(plugin.path, from: packagesDir.path); - print('Start running for $packageName ...'); - if (!isIosPlugin(plugin, fileSystem)) { - print('iOS is not supported by this plugin.'); - print('\n\n'); - continue; - } - if (skipped.contains(packageName)) { - print('$packageName was skipped with the --skip flag.'); - print('\n\n'); - continue; - } - for (Directory example in getExamplesForPlugin(plugin)) { - // Look for the test scheme in the example app. - print('Look for target named: $_kTarget ...'); - final List findSchemeArgs = [ - '-project', - 'ios/Runner.xcodeproj', - '-list', - '-json' - ]; - final String completeFindSchemeCommand = - '$_kXcodeBuildCommand ${findSchemeArgs.join(' ')}'; - print(completeFindSchemeCommand); - final io.ProcessResult xcodeprojListResult = await processRunner - .run(_kXcodeBuildCommand, findSchemeArgs, workingDir: example); - if (xcodeprojListResult.exitCode != 0) { - print('Error occurred while running "$completeFindSchemeCommand":\n' - '${xcodeprojListResult.stderr}'); - failingPackages.add(packageName); - print('\n\n'); - continue; - } - - final String xcodeprojListOutput = xcodeprojListResult.stdout; - Map xcodeprojListOutputJson = - jsonDecode(xcodeprojListOutput); - if (!xcodeprojListOutputJson['project']['targets'].contains(target)) { - failingPackages.add(packageName); - print('$target not configured for $packageName, test failed.'); - print( - 'Please check the scheme for the test target if it matches the name $target.\n' - 'If this plugin does not have an XCTest target, use the $_kSkip flag in the $name command to skip the plugin.'); - print('\n\n'); - continue; - } - // Found the scheme, running tests - print('Running XCTests:$target for $packageName ...'); - final List xctestArgs = [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - target, - '-destination', - destination, - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ]; - final String completeTestCommand = - '$_kXcodeBuildCommand ${xctestArgs.join(' ')}'; - print(completeTestCommand); - final int exitCode = await processRunner - .runAndStream(_kXcodeBuildCommand, xctestArgs, workingDir: example); - if (exitCode == 0) { - print('Successfully ran xctest for $packageName'); - } else { - failingPackages.add(packageName); - } - } - } - - // Command end, print reports. - if (failingPackages.isEmpty) { - print("All XCTests have passed!"); - } else { - print( - 'The following packages are failing XCTests (see above for details):'); - for (String package in failingPackages) { - print(' * $package'); - } - throw ToolExit(1); - } - } - - Future _findAvailableIphoneSimulator() async { - // Find the first available destination if not specified. - final List findSimulatorsArguments = [ - 'simctl', - 'list', - '--json' - ]; - final String findSimulatorCompleteCommand = - '$_kXCRunCommand ${findSimulatorsArguments.join(' ')}'; - print('Looking for available simulators...'); - print(findSimulatorCompleteCommand); - final io.ProcessResult findSimulatorsResult = - await processRunner.run(_kXCRunCommand, findSimulatorsArguments); - if (findSimulatorsResult.exitCode != 0) { - print('Error occurred while running "$findSimulatorCompleteCommand":\n' - '${findSimulatorsResult.stderr}'); - throw ToolExit(1); - } - final Map simulatorListJson = - jsonDecode(findSimulatorsResult.stdout); - final List runtimes = simulatorListJson['runtimes']; - final Map devices = simulatorListJson['devices']; - if (runtimes.isEmpty || devices.isEmpty) { - return null; - } - String id; - // Looking for runtimes, trying to find one with highest OS version. - for (Map runtimeMap in runtimes.reversed) { - if (!runtimeMap['name'].contains('iOS')) { - continue; - } - final String runtimeID = runtimeMap['identifier']; - final List devicesForRuntime = devices[runtimeID]; - if (devicesForRuntime.isEmpty) { - continue; - } - // Looking for runtimes, trying to find latest version of device. - for (Map device in devicesForRuntime.reversed) { - if (device['availabilityError'] != null || - (device['isAvailable'] as bool == false)) { - continue; - } - id = device['udid']; - print('device selected: $device'); - return id; - } - } - return null; - } -} diff --git a/packages/flutter_plugin_tools/pubspec.yaml b/packages/flutter_plugin_tools/pubspec.yaml deleted file mode 100644 index f8b452910236..000000000000 --- a/packages/flutter_plugin_tools/pubspec.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: flutter_plugin_tools -description: Productivity utils for hosting multiple plugins within one repository. -homepage: https://github.com/flutter/plugin_tools -version: 0.0.45+1 - -dependencies: - args: "^1.4.3" - path: "^1.6.1" - http: "^0.12.1" - async: "^2.0.7" - yaml: "^2.1.15" - quiver: "^2.0.2" - pub_semver: ^1.4.2 - colorize: ^2.0.0 - git: ^1.0.0 - platform: ^2.2.0 - pubspec_parse: "^0.1.4" - test: ^1.6.4 - meta: ^1.1.7 - file: ^5.0.10 - uuid: ^2.0.4 - http_multi_server: ^2.2.0 - -dev_dependencies: - matcher: ^0.12.6 - mockito: ^4.1.1 - pedantic: 1.8.0 - -environment: - sdk: ">=2.3.0 <3.0.0" diff --git a/packages/flutter_plugin_tools/test/analyze_command_test.dart b/packages/flutter_plugin_tools/test/analyze_command_test.dart deleted file mode 100644 index 9e7a42bbb680..000000000000 --- a/packages/flutter_plugin_tools/test/analyze_command_test.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/analyze_command.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - RecordingProcessRunner processRunner; - CommandRunner runner; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final AnalyzeCommand analyzeCommand = AnalyzeCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner('analyze_command', 'Test for analyze_command'); - runner.addCommand(analyzeCommand); - }); - - tearDown(() { - mockPackagesDir.deleteSync(recursive: true); - }); - - test('analyzes all packages', () async { - final Directory plugin1Dir = await createFakePlugin('a'); - final Directory plugin2Dir = await createFakePlugin('b'); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - await runner.run(['analyze']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('pub', ['global', 'activate', 'tuneup'], - mockPackagesDir.path), - ProcessCall('flutter', ['packages', 'get'], plugin1Dir.path), - ProcessCall('flutter', ['packages', 'get'], plugin2Dir.path), - ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], - plugin1Dir.path), - ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], - plugin2Dir.path), - ])); - }); - - group('verifies analysis settings', () { - test('fails analysis_options.yaml', () async { - await createFakePlugin('foo', withExtraFiles: >[ - ['analysis_options.yaml'] - ]); - - await expectLater(() => runner.run(['analyze']), - throwsA(const TypeMatcher())); - }); - - test('fails .analysis_options', () async { - await createFakePlugin('foo', withExtraFiles: >[ - ['.analysis_options'] - ]); - - await expectLater(() => runner.run(['analyze']), - throwsA(const TypeMatcher())); - }); - - test('takes an allow list', () async { - final Directory pluginDir = - await createFakePlugin('foo', withExtraFiles: >[ - ['analysis_options.yaml'] - ]); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - await runner.run(['analyze', '--custom-analysis', 'foo']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('pub', ['global', 'activate', 'tuneup'], - mockPackagesDir.path), - ProcessCall('flutter', ['packages', 'get'], pluginDir.path), - ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], - pluginDir.path), - ])); - }); - }); -} diff --git a/packages/flutter_plugin_tools/test/build_examples_command_test.dart b/packages/flutter_plugin_tools/test/build_examples_command_test.dart deleted file mode 100644 index eaf5049dcc02..000000000000 --- a/packages/flutter_plugin_tools/test/build_examples_command_test.dart +++ /dev/null @@ -1,470 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/build_examples_command.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - group('test build_example_command', () { - CommandRunner runner; - RecordingProcessRunner processRunner; - final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final BuildExamplesCommand command = BuildExamplesCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner( - 'build_examples_command', 'Test for build_example_command'); - runner.addCommand(command); - cleanupPackages(); - }); - - test('building for iOS when plugin is not set up for iOS results in no-op', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--ipa', '--no-macos']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING IPA for $packageName', - 'iOS is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - - test('building for ios', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'build-examples', - '--ipa', - '--no-macos', - '--enable-experiment=exp1' - ]); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING IPA for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'build', - 'ios', - '--no-codesign', - '--enable-experiment=exp1' - ], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test( - 'building for Linux when plugin is not set up for Linux results in no-op', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--linux']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING Linux for $packageName', - 'Linux is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --linux with no - // Linux implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - - test('building for Linux', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--linux']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING Linux for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(flutterCommand, ['build', 'linux'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test('building for macos with no implementation results in no-op', - () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['example', 'test'], - ]); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--macos']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING macOS for $packageName', - '\macOS is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - test('building for macos', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ['example', 'macos', 'macos.swift'], - ], - isMacOsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--macos']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING macOS for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(flutterCommand, ['pub', 'get'], - pluginExampleDirectory.path), - ProcessCall(flutterCommand, ['build', 'macos'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test( - 'building for Windows when plugin is not set up for Windows results in no-op', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isWindowsPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--windows']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING Windows for $packageName', - 'Windows is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - - test('building for windows', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isWindowsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--windows']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING Windows for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(flutterCommand, ['build', 'windows'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test( - 'building for Android when plugin is not set up for Android results in no-op', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--apk', '--no-ipa']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING APK for $packageName', - 'Android is not supported by this plugin', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); - }); - - test('building for android', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isAndroidPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'build-examples', - '--apk', - '--no-ipa', - '--no-macos', - ]); - final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); - - expect( - output, - orderedEquals([ - '\nBUILDING APK for $packageName', - '\n\n', - 'All builds successful!', - ]), - ); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(flutterCommand, ['build', 'apk'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test('enable-experiment flag for Android', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isAndroidPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - await runCapturingPrint(runner, [ - 'build-examples', - '--apk', - '--no-ipa', - '--no-macos', - '--enable-experiment=exp1' - ]); - - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - ['build', 'apk', '--enable-experiment=exp1'], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - - test('enable-experiment flag for ios', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - await runCapturingPrint(runner, [ - 'build-examples', - '--ipa', - '--no-macos', - '--enable-experiment=exp1' - ]); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'build', - 'ios', - '--no-codesign', - '--enable-experiment=exp1' - ], - pluginExampleDirectory.path), - ])); - cleanupPackages(); - }); - }); -} diff --git a/packages/flutter_plugin_tools/test/common_test.dart b/packages/flutter_plugin_tools/test/common_test.dart deleted file mode 100644 index b3504c2358d9..000000000000 --- a/packages/flutter_plugin_tools/test/common_test.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - RecordingProcessRunner processRunner; - CommandRunner runner; - List plugins; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - plugins = []; - final SamplePluginCommand samplePluginCommand = SamplePluginCommand( - plugins, - mockPackagesDir, - mockFileSystem, - processRunner: processRunner, - ); - runner = - CommandRunner('common_command', 'Test for common functionality'); - runner.addCommand(samplePluginCommand); - }); - - tearDown(() { - mockPackagesDir.deleteSync(recursive: true); - }); - - test('all plugins from file system', () async { - final Directory plugin1 = createFakePlugin('plugin1'); - final Directory plugin2 = createFakePlugin('plugin2'); - await runner.run(['sample']); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); - }); - - test('exclude plugins when plugins flag is specified', () async { - createFakePlugin('plugin1'); - final Directory plugin2 = createFakePlugin('plugin2'); - await runner.run( - ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); - expect(plugins, unorderedEquals([plugin2.path])); - }); - - test('exclude plugins when plugins flag isn\'t specified', () async { - createFakePlugin('plugin1'); - createFakePlugin('plugin2'); - await runner.run(['sample', '--exclude=plugin1,plugin2']); - expect(plugins, unorderedEquals([])); - }); - - test('exclude federated plugins when plugins flag is specified', () async { - createFakePlugin('plugin1', parentDirectoryName: 'federated'); - final Directory plugin2 = createFakePlugin('plugin2'); - await runner.run([ - 'sample', - '--plugins=federated/plugin1,plugin2', - '--exclude=federated/plugin1' - ]); - expect(plugins, unorderedEquals([plugin2.path])); - }); - - test('exclude entire federated plugins when plugins flag is specified', - () async { - createFakePlugin('plugin1', parentDirectoryName: 'federated'); - final Directory plugin2 = createFakePlugin('plugin2'); - await runner.run([ - 'sample', - '--plugins=federated/plugin1,plugin2', - '--exclude=federated' - ]); - expect(plugins, unorderedEquals([plugin2.path])); - }); -} - -class SamplePluginCommand extends PluginCommand { - SamplePluginCommand( - this.plugins_, - Directory packagesDir, - FileSystem fileSystem, { - ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner); - - List plugins_; - - @override - final String name = 'sample'; - - @override - final String description = 'sample command'; - - @override - Future run() async { - await for (Directory package in getPlugins()) { - this.plugins_.add(package.path); - } - } -} diff --git a/packages/flutter_plugin_tools/test/drive_examples_command_test.dart b/packages/flutter_plugin_tools/test/drive_examples_command_test.dart deleted file mode 100644 index f4bdd95c1664..000000000000 --- a/packages/flutter_plugin_tools/test/drive_examples_command_test.dart +++ /dev/null @@ -1,505 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - group('test drive_example_command', () { - CommandRunner runner; - RecordingProcessRunner processRunner; - final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final DriveExamplesCommand command = DriveExamplesCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner( - 'drive_examples_command', 'Test for drive_example_command'); - runner.addCommand(command); - }); - - tearDown(() { - cleanupPackages(); - }); - - test('driving under folder "test"', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test', 'plugin.dart'], - ], - isIosPlugin: true, - isAndroidPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving under folder "test_driver"', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isAndroidPlugin: true, - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving under folder "test_driver" when test files are missing"', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ], - isAndroidPlugin: true, - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - await expectLater( - () => runCapturingPrint(runner, ['drive-examples']), - throwsA(const TypeMatcher())); - }); - - test( - 'driving under folder "test_driver" when targets are under "integration_test"', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'integration_test.dart'], - ['example', 'integration_test', 'bar_test.dart'], - ['example', 'integration_test', 'foo_test.dart'], - ['example', 'integration_test', 'ignore_me.dart'], - ], - isAndroidPlugin: true, - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String driverTestPath = p.join('test_driver', 'integration_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - p.join('integration_test', 'bar_test.dart'), - ], - pluginExampleDirectory.path), - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - p.join('integration_test', 'foo_test.dart'), - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not support Linux is a no-op', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isMacOsPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--linux', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running drive-examples --linux on a non-Linux - // plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving on a Linux plugin', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isLinuxPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--linux', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '-d', - 'linux', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not suppport macOS is a no-op', () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ]); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--macos', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running drive-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, []); - }); - test('driving on a macOS plugin', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ['example', 'macos', 'macos.swift'], - ], - isMacOsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--macos', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '-d', - 'macos', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not suppport windows is a no-op', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isMacOsPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--windows', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running drive-examples --windows on a non-windows - // plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving on a Windows plugin', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isWindowsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--windows', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '-d', - 'windows', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not support mobile is no-op', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isMacOsPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); - - expect( - output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', - ]), - ); - - print(processRunner.recordedCalls); - // Output should be empty since running drive-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('enable-experiment flag', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test', 'plugin.dart'], - ], - isIosPlugin: true, - isAndroidPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - await runCapturingPrint(runner, [ - 'drive-examples', - '--enable-experiment=exp1', - ]); - - String deviceTestPath = p.join('test', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--enable-experiment=exp1', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); - }); - }); -} diff --git a/packages/flutter_plugin_tools/test/firebase_test_lab_test.dart b/packages/flutter_plugin_tools/test/firebase_test_lab_test.dart deleted file mode 100644 index 97b977619d57..000000000000 --- a/packages/flutter_plugin_tools/test/firebase_test_lab_test.dart +++ /dev/null @@ -1,256 +0,0 @@ -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$FirebaseTestLabCommand', () { - final List printedMessages = []; - CommandRunner runner; - RecordingProcessRunner processRunner; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final FirebaseTestLabCommand command = FirebaseTestLabCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner, - print: (Object message) => printedMessages.add(message.toString())); - - runner = CommandRunner( - 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); - runner.addCommand(command); - }); - - tearDown(() { - printedMessages.clear(); - }); - - test('retries gcloud set', () async { - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(1); - processRunner.processToReturn = mockProcess; - createFakePlugin('plugin', withExtraFiles: >[ - ['lib/test/should_not_run_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e_test.dart'], - ['example', 'android', 'gradlew'], - ['example', 'should_not_run_e2e.dart'], - [ - 'example', - 'android', - 'app', - 'src', - 'androidTest', - 'MainActivityTest.java' - ], - ]); - await expectLater( - () => runCapturingPrint(runner, ['firebase-test-lab']), - throwsA(const TypeMatcher())); - expect( - printedMessages, - contains( - "\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.")); - }); - - test('runs e2e tests', () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['test', 'plugin_test.dart'], - ['test', 'plugin_e2e.dart'], - ['should_not_run_e2e.dart'], - ['lib/test/should_not_run_e2e.dart'], - ['example', 'test', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e_test.dart'], - ['example', 'integration_test', 'foo_test.dart'], - ['example', 'integration_test', 'should_not_run.dart'], - ['example', 'android', 'gradlew'], - ['example', 'should_not_run_e2e.dart'], - [ - 'example', - 'android', - 'app', - 'src', - 'androidTest', - 'MainActivityTest.java' - ], - ]); - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=flame,version=29', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - ]); - - expect( - printedMessages, - orderedEquals([ - '\nRUNNING FIREBASE TEST LAB TESTS for plugin', - '\nFirebase project configured.', - '\n\n', - 'All Firebase Test Lab tests successful!', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true'.split(' '), - '/packages/plugin/example/android'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ]), - ); - }); - - test('experimental flag', () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['test', 'plugin_test.dart'], - ['test', 'plugin_e2e.dart'], - ['should_not_run_e2e.dart'], - ['lib/test/should_not_run_e2e.dart'], - ['example', 'test', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e_test.dart'], - ['example', 'integration_test', 'foo_test.dart'], - ['example', 'integration_test', 'should_not_run.dart'], - ['example', 'android', 'gradlew'], - ['example', 'should_not_run_e2e.dart'], - [ - 'example', - 'android', - 'app', - 'src', - 'androidTest', - 'MainActivityTest.java' - ], - ]); - - await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=flame,version=29', - '--test-run-id', - 'testRunId', - '--enable-experiment=exp1', - ]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ]), - ); - - cleanupPackages(); - }); - }); -} diff --git a/packages/flutter_plugin_tools/test/lint_podspecs_command_test.dart b/packages/flutter_plugin_tools/test/lint_podspecs_command_test.dart deleted file mode 100644 index 49d6ad4d8e20..000000000000 --- a/packages/flutter_plugin_tools/test/lint_podspecs_command_test.dart +++ /dev/null @@ -1,202 +0,0 @@ -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$LintPodspecsCommand', () { - CommandRunner runner; - MockPlatform mockPlatform; - final RecordingProcessRunner processRunner = RecordingProcessRunner(); - List printedMessages; - - setUp(() { - initializeFakePackages(); - - printedMessages = []; - mockPlatform = MockPlatform(); - when(mockPlatform.isMacOS).thenReturn(true); - final LintPodspecsCommand command = LintPodspecsCommand( - mockPackagesDir, - mockFileSystem, - processRunner: processRunner, - platform: mockPlatform, - print: (Object message) => printedMessages.add(message.toString()), - ); - - runner = - CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); - runner.addCommand(command); - final MockProcess mockLintProcess = MockProcess(); - mockLintProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockLintProcess; - processRunner.recordedCalls.clear(); - }); - - tearDown(() { - cleanupPackages(); - }); - - test('only runs on macOS', () async { - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'], - ]); - - when(mockPlatform.isMacOS).thenReturn(false); - await runner.run(['podspecs']); - - expect( - processRunner.recordedCalls, - equals([]), - ); - }); - - test('runs pod lib lint on a podspec', () async { - Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['ios', 'plugin1.podspec'], - ['bogus.dart'], // Ignore non-podspecs. - ]); - - processRunner.resultStdout = 'Foo'; - processRunner.resultStderr = 'Bar'; - - await runner.run(['podspecs']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), - '--analyze', - '--use-libraries' - ], - mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), - '--analyze', - ], - mockPackagesDir.path), - ]), - ); - - expect( - printedMessages, contains('Linting and analyzing plugin1.podspec')); - expect(printedMessages, contains('Foo')); - expect(printedMessages, contains('Bar')); - }); - - test('skips podspecs with known issues', () async { - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'] - ]); - createFakePlugin('plugin2', withExtraFiles: >[ - ['plugin2.podspec'] - ]); - - await runner - .run(['podspecs', '--skip=plugin1', '--skip=plugin2']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), - ]), - ); - }); - - test('skips analyzer for podspecs with known warnings', () async { - Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'], - ]); - - await runner.run(['podspecs', '--no-analyze=plugin1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - '--use-libraries' - ], - mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - ], - mockPackagesDir.path), - ]), - ); - - expect(printedMessages, contains('Linting plugin1.podspec')); - }); - - test('allow warnings for podspecs with known warnings', () async { - Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'], - ]); - - await runner.run(['podspecs', '--ignore-warnings=plugin1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - '--allow-warnings', - '--analyze', - '--use-libraries' - ], - mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - '--allow-warnings', - '--analyze', - ], - mockPackagesDir.path), - ]), - ); - - expect( - printedMessages, contains('Linting and analyzing plugin1.podspec')); - }); - }); -} - -class MockPlatform extends Mock implements Platform {} diff --git a/packages/flutter_plugin_tools/test/list_command_test.dart b/packages/flutter_plugin_tools/test/list_command_test.dart deleted file mode 100644 index 478625283dd0..000000000000 --- a/packages/flutter_plugin_tools/test/list_command_test.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/list_command.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - group('$ListCommand', () { - CommandRunner runner; - - setUp(() { - initializeFakePackages(); - final ListCommand command = ListCommand(mockPackagesDir, mockFileSystem); - - runner = CommandRunner('list_test', 'Test for $ListCommand'); - runner.addCommand(command); - }); - - test('lists plugins', () async { - createFakePlugin('plugin1'); - createFakePlugin('plugin2'); - - final List plugins = - await runCapturingPrint(runner, ['list', '--type=plugin']); - - expect( - plugins, - orderedEquals([ - '/packages/plugin1', - '/packages/plugin2', - ]), - ); - - cleanupPackages(); - }); - - test('lists examples', () async { - createFakePlugin('plugin1', withSingleExample: true); - createFakePlugin('plugin2', - withExamples: ['example1', 'example2']); - createFakePlugin('plugin3'); - - final List examples = - await runCapturingPrint(runner, ['list', '--type=example']); - - expect( - examples, - orderedEquals([ - '/packages/plugin1/example', - '/packages/plugin2/example/example1', - '/packages/plugin2/example/example2', - ]), - ); - - cleanupPackages(); - }); - - test('lists packages', () async { - createFakePlugin('plugin1', withSingleExample: true); - createFakePlugin('plugin2', - withExamples: ['example1', 'example2']); - createFakePlugin('plugin3'); - - final List packages = - await runCapturingPrint(runner, ['list', '--type=package']); - - expect( - packages, - unorderedEquals([ - '/packages/plugin1', - '/packages/plugin1/example', - '/packages/plugin2', - '/packages/plugin2/example/example1', - '/packages/plugin2/example/example2', - '/packages/plugin3', - ]), - ); - - cleanupPackages(); - }); - - test('lists files', () async { - createFakePlugin('plugin1', withSingleExample: true); - createFakePlugin('plugin2', - withExamples: ['example1', 'example2']); - createFakePlugin('plugin3'); - - final List examples = - await runCapturingPrint(runner, ['list', '--type=file']); - - expect( - examples, - unorderedEquals([ - '/packages/plugin1/pubspec.yaml', - '/packages/plugin1/example/pubspec.yaml', - '/packages/plugin2/pubspec.yaml', - '/packages/plugin2/example/example1/pubspec.yaml', - '/packages/plugin2/example/example2/pubspec.yaml', - '/packages/plugin3/pubspec.yaml', - ]), - ); - - cleanupPackages(); - }); - - test('lists plugins using federated plugin layout', () async { - createFakePlugin('plugin1'); - - // Create a federated plugin by creating a directory under the packages - // directory with several packages underneath. - final Directory federatedPlugin = - mockPackagesDir.childDirectory('my_plugin')..createSync(); - final Directory clientLibrary = - federatedPlugin.childDirectory('my_plugin')..createSync(); - createFakePubspec(clientLibrary); - final Directory webLibrary = - federatedPlugin.childDirectory('my_plugin_web')..createSync(); - createFakePubspec(webLibrary); - final Directory macLibrary = - federatedPlugin.childDirectory('my_plugin_macos')..createSync(); - createFakePubspec(macLibrary); - - // Test without specifying `--type`. - final List plugins = - await runCapturingPrint(runner, ['list']); - - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - '/packages/my_plugin/my_plugin', - '/packages/my_plugin/my_plugin_web', - '/packages/my_plugin/my_plugin_macos', - ]), - ); - - cleanupPackages(); - }); - - test('can filter plugins with the --plugins argument', () async { - createFakePlugin('plugin1'); - - // Create a federated plugin by creating a directory under the packages - // directory with several packages underneath. - final Directory federatedPlugin = - mockPackagesDir.childDirectory('my_plugin')..createSync(); - final Directory clientLibrary = - federatedPlugin.childDirectory('my_plugin')..createSync(); - createFakePubspec(clientLibrary); - final Directory webLibrary = - federatedPlugin.childDirectory('my_plugin_web')..createSync(); - createFakePubspec(webLibrary); - final Directory macLibrary = - federatedPlugin.childDirectory('my_plugin_macos')..createSync(); - createFakePubspec(macLibrary); - - List plugins = await runCapturingPrint( - runner, ['list', '--plugins=plugin1']); - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - ]), - ); - - plugins = await runCapturingPrint( - runner, ['list', '--plugins=my_plugin']); - expect( - plugins, - unorderedEquals([ - '/packages/my_plugin/my_plugin', - '/packages/my_plugin/my_plugin_web', - '/packages/my_plugin/my_plugin_macos', - ]), - ); - - plugins = await runCapturingPrint( - runner, ['list', '--plugins=my_plugin/my_plugin_web']); - expect( - plugins, - unorderedEquals([ - '/packages/my_plugin/my_plugin_web', - ]), - ); - - plugins = await runCapturingPrint(runner, - ['list', '--plugins=my_plugin/my_plugin_web,plugin1']); - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - '/packages/my_plugin/my_plugin_web', - ]), - ); - }); - }); -} diff --git a/packages/flutter_plugin_tools/test/mocks.dart b/packages/flutter_plugin_tools/test/mocks.dart deleted file mode 100644 index 3e17ff8efd32..000000000000 --- a/packages/flutter_plugin_tools/test/mocks.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:async'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:mockito/mockito.dart'; - -class MockProcess extends Mock implements io.Process { - final Completer exitCodeCompleter = Completer(); - final StreamController> stdoutController = - StreamController>(); - final StreamController> stderrController = - StreamController>(); - final MockIOSink stdinMock = MockIOSink(); - - @override - Future get exitCode => exitCodeCompleter.future; - - @override - Stream> get stdout => stdoutController.stream; - - @override - Stream> get stderr => stderrController.stream; - - @override - IOSink get stdin => stdinMock; -} - -class MockIOSink extends Mock implements IOSink { - List lines = []; - - @override - void writeln([Object obj = ""]) => lines.add(obj); -} diff --git a/packages/flutter_plugin_tools/test/publish_plugin_command_test.dart b/packages/flutter_plugin_tools/test/publish_plugin_command_test.dart deleted file mode 100644 index d01bfa67ce24..000000000000 --- a/packages/flutter_plugin_tools/test/publish_plugin_command_test.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:git/git.dart'; -import 'package:matcher/matcher.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - const String testPluginName = 'foo'; - final List printedMessages = []; - - Directory parentDir; - Directory pluginDir; - GitDir gitDir; - TestProcessRunner processRunner; - CommandRunner commandRunner; - MockStdin mockStdin; - - setUp(() async { - // This test uses a local file system instead of an in memory one throughout - // so that git actually works. In setup we initialize a mono repo of plugins - // with one package and commit everything to Git. - parentDir = const LocalFileSystem() - .systemTempDirectory - .createTempSync('publish_plugin_command_test-'); - initializeFakePackages(parentDir: parentDir); - pluginDir = createFakePlugin(testPluginName, withSingleExample: false); - assert(pluginDir != null && pluginDir.existsSync()); - createFakePubspec(pluginDir, includeVersion: true); - io.Process.runSync('git', ['init'], - workingDirectory: mockPackagesDir.path); - gitDir = await GitDir.fromExisting(mockPackagesDir.path); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Initial commit']); - processRunner = TestProcessRunner(); - mockStdin = MockStdin(); - commandRunner = CommandRunner('tester', '') - ..addCommand(PublishPluginCommand( - mockPackagesDir, const LocalFileSystem(), - processRunner: processRunner, - print: (Object message) => printedMessages.add(message.toString()), - stdinput: mockStdin)); - }); - - tearDown(() { - parentDir.deleteSync(recursive: true); - printedMessages.clear(); - }); - - group('Initial validation', () { - test('requires a package flag', () async { - await expectLater(() => commandRunner.run(['publish-plugin']), - throwsA(const TypeMatcher())); - - expect( - printedMessages.last, contains("Must specify a package to publish.")); - }); - - test('requires an existing flag', () async { - await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', 'iamerror']), - throwsA(const TypeMatcher())); - - expect(printedMessages.last, contains('iamerror does not exist')); - }); - - test('refuses to proceed with dirty files', () async { - pluginDir.childFile('tmp').createSync(); - - await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', testPluginName]), - throwsA(const TypeMatcher())); - - expect( - printedMessages.last, - contains( - "There are files in the package directory that haven't been saved in git.")); - }); - - test('fails immediately if the remote doesn\'t exist', () async { - await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', testPluginName]), - throwsA(const TypeMatcher())); - - expect(processRunner.results.last.stderr, contains("No such remote")); - }); - - test("doesn't validate the remote if it's not pushing tags", () async { - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release' - ]); - - expect(printedMessages.last, 'Done!'); - }); - }); - - group('Publishes package', () { - test('while showing all output from pub publish to the user', () async { - final Future publishCommand = commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release' - ]); - processRunner.mockPublishProcess.stdoutController.add(utf8.encode('Foo')); - processRunner.mockPublishProcess.stderrController.add(utf8.encode('Bar')); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - - await publishCommand; - - expect(printedMessages, contains('Foo')); - expect(printedMessages, contains('Bar')); - }); - - test('forwards input from the user to `pub publish`', () async { - final Future publishCommand = commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release' - ]); - mockStdin.controller.add(utf8.encode('user input')); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - - await publishCommand; - - expect(processRunner.mockPublishProcess.stdinMock.lines, - contains('user input')); - }); - - test('forwards --pub-publish-flags to pub publish', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release', - '--pub-publish-flags', - '--dry-run,--server=foo' - ]); - - expect(processRunner.mockPublishArgs.length, 4); - expect(processRunner.mockPublishArgs[0], 'pub'); - expect(processRunner.mockPublishArgs[1], 'publish'); - expect(processRunner.mockPublishArgs[2], '--dry-run'); - expect(processRunner.mockPublishArgs[3], '--server=foo'); - }); - - test('throws if pub publish fails', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(128); - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release', - ]), - throwsA(const TypeMatcher())); - - expect(printedMessages, contains("Publish failed. Exiting.")); - }); - }); - - group('Tags release', () { - test('with the version and name from the pubspec.yaml', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - ]); - - final String tag = - (await gitDir.runCommand(['show-ref', 'fake_package-v0.0.1'])) - .stdout; - expect(tag, isNotEmpty); - }); - - test('only if publishing succeeded', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(128); - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - ]), - throwsA(const TypeMatcher())); - - expect(printedMessages, contains("Publish failed. Exiting.")); - final String tag = (await gitDir.runCommand( - ['show-ref', 'fake_package-v0.0.1'], - throwOnError: false)) - .stdout; - expect(tag, isEmpty); - }); - }); - - group('Pushes tags', () { - setUp(() async { - await gitDir.runCommand( - ['remote', 'add', 'upstream', 'http://localhost:8000']); - }); - - test('requires user confirmation', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - mockStdin.readLineOutput = 'help'; - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - ]), - throwsA(const TypeMatcher())); - - expect(printedMessages, contains('Tag push canceled.')); - }); - - test('to upstream by default', () async { - await gitDir.runCommand(['tag', 'garbage']); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - mockStdin.readLineOutput = 'y'; - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - ]); - - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); - expect(printedMessages.last, 'Done!'); - }); - - test('to different remotes based on a flag', () async { - await gitDir.runCommand( - ['remote', 'add', 'origin', 'http://localhost:8001']); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - mockStdin.readLineOutput = 'y'; - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--remote', - 'origin', - ]); - - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'origin'); - expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); - expect(printedMessages.last, 'Done!'); - }); - - test('only if tagging and pushing to remotes are both enabled', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); - await commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-tag-release', - ]); - - expect(processRunner.pushTagsArgs.isEmpty, isTrue); - expect(printedMessages.last, 'Done!'); - }); - }); -} - -class TestProcessRunner extends ProcessRunner { - final List results = []; - final MockProcess mockPublishProcess = MockProcess(); - final List mockPublishArgs = []; - final MockProcessResult mockPushTagsResult = MockProcessResult(); - final List pushTagsArgs = []; - - @override - Future runAndExitOnError( - String executable, - List args, { - Directory workingDir, - }) async { - // Don't ever really push tags. - if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { - pushTagsArgs.addAll(args); - return mockPushTagsResult; - } - - final io.ProcessResult result = io.Process.runSync(executable, args, - workingDirectory: workingDir?.path); - results.add(result); - if (result.exitCode != 0) { - throw ToolExit(result.exitCode); - } - return result; - } - - @override - Future start(String executable, List args, - {Directory workingDirectory}) async { - /// Never actually publish anything. Start is always and only used for this - /// since it returns something we can route stdin through. - assert(executable == 'flutter' && - args.isNotEmpty && - args[0] == 'pub' && - args[1] == 'publish'); - mockPublishArgs.addAll(args); - return mockPublishProcess; - } -} - -class MockStdin extends Mock implements io.Stdin { - final StreamController> controller = StreamController>(); - String readLineOutput; - - @override - Stream transform(StreamTransformer streamTransformer) { - return controller.stream.transform(streamTransformer); - } - - @override - StreamSubscription> listen(void onData(List event), - {Function onError, void onDone(), bool cancelOnError}) { - return controller.stream.listen(onData, - onError: onError, onDone: onDone, cancelOnError: cancelOnError); - } - - @override - String readLineSync( - {Encoding encoding = io.systemEncoding, - bool retainNewlines = false}) => - readLineOutput; -} - -class MockProcessResult extends Mock implements io.ProcessResult {} diff --git a/packages/flutter_plugin_tools/test/test_command_test.dart b/packages/flutter_plugin_tools/test/test_command_test.dart deleted file mode 100644 index 514e4c27190a..000000000000 --- a/packages/flutter_plugin_tools/test/test_command_test.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/test_command.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - group('$TestCommand', () { - CommandRunner runner; - final RecordingProcessRunner processRunner = RecordingProcessRunner(); - - setUp(() { - initializeFakePackages(); - final TestCommand command = TestCommand(mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner('test_test', 'Test for $TestCommand'); - runner.addCommand(command); - }); - - tearDown(() { - cleanupPackages(); - processRunner.recordedCalls.clear(); - }); - - test('runs flutter test on each plugin', () async { - final Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = - createFakePlugin('plugin2', withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - - await runner.run(['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', ['test', '--color'], plugin1Dir.path), - ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), - ]), - ); - - cleanupPackages(); - }); - - test('skips testing plugins without test directory', () async { - createFakePlugin('plugin1'); - final Directory plugin2Dir = - createFakePlugin('plugin2', withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - - await runner.run(['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), - ]), - ); - - cleanupPackages(); - }); - - test('runs pub run test on non-Flutter packages', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', - isFlutter: true, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = createFakePlugin('plugin2', - isFlutter: false, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - - await runner.run(['test', '--enable-experiment=exp1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - ['test', '--color', '--enable-experiment=exp1'], - plugin1Dir.path), - ProcessCall('pub', ['get'], plugin2Dir.path), - ProcessCall( - 'pub', - ['run', '--enable-experiment=exp1', 'test'], - plugin2Dir.path), - ]), - ); - - cleanupPackages(); - }); - - test('runs on Chrome for web plugins', () async { - final Directory pluginDir = createFakePlugin( - 'plugin', - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ], - isFlutter: true, - isWebPlugin: true, - ); - - await runner.run(['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', - ['test', '--color', '--platform=chrome'], pluginDir.path), - ]), - ); - }); - - test('enable-experiment flag', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', - isFlutter: true, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = createFakePlugin('plugin2', - isFlutter: false, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - - await runner.run(['test', '--enable-experiment=exp1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - ['test', '--color', '--enable-experiment=exp1'], - plugin1Dir.path), - ProcessCall('pub', ['get'], plugin2Dir.path), - ProcessCall( - 'pub', - ['run', '--enable-experiment=exp1', 'test'], - plugin2Dir.path), - ]), - ); - - cleanupPackages(); - }); - }); -} diff --git a/packages/flutter_plugin_tools/test/util.dart b/packages/flutter_plugin_tools/test/util.dart deleted file mode 100644 index ec0000d13f34..000000000000 --- a/packages/flutter_plugin_tools/test/util.dart +++ /dev/null @@ -1,291 +0,0 @@ -import 'dart:async'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:platform/platform.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; -import 'package:quiver/collection.dart'; - -FileSystem mockFileSystem = MemoryFileSystem( - style: LocalPlatform().isWindows - ? FileSystemStyle.windows - : FileSystemStyle.posix); -Directory mockPackagesDir; - -/// Creates a mock packages directory in the mock file system. -/// -/// If [parentDir] is set the mock packages dir will be creates as a child of -/// it. If not [mockFileSystem] will be used instead. -void initializeFakePackages({Directory parentDir}) { - mockPackagesDir = - (parentDir ?? mockFileSystem.currentDirectory).childDirectory('packages'); - mockPackagesDir.createSync(); -} - -/// Creates a plugin package with the given [name] in [mockPackagesDir]. -Directory createFakePlugin( - String name, { - bool withSingleExample = false, - List withExamples = const [], - List> withExtraFiles = const >[], - bool isFlutter = true, - bool isAndroidPlugin = false, - bool isIosPlugin = false, - bool isWebPlugin = false, - bool isLinuxPlugin = false, - bool isMacOsPlugin = false, - bool isWindowsPlugin = false, - String parentDirectoryName = '', -}) { - assert(!(withSingleExample && withExamples.isNotEmpty), - 'cannot pass withSingleExample and withExamples simultaneously'); - - final Directory pluginDirectory = (parentDirectoryName != '') - ? mockPackagesDir.childDirectory(parentDirectoryName).childDirectory(name) - : mockPackagesDir.childDirectory(name); - pluginDirectory.createSync(recursive: true); - - createFakePubspec( - pluginDirectory, - name: name, - isFlutter: isFlutter, - isAndroidPlugin: isAndroidPlugin, - isIosPlugin: isIosPlugin, - isWebPlugin: isWebPlugin, - isLinuxPlugin: isLinuxPlugin, - isMacOsPlugin: isMacOsPlugin, - isWindowsPlugin: isWindowsPlugin, - ); - - if (withSingleExample) { - final Directory exampleDir = pluginDirectory.childDirectory('example') - ..createSync(); - createFakePubspec(exampleDir, - name: "${name}_example", isFlutter: isFlutter); - } else if (withExamples.isNotEmpty) { - final Directory exampleDir = pluginDirectory.childDirectory('example') - ..createSync(); - for (String example in withExamples) { - final Directory currentExample = exampleDir.childDirectory(example) - ..createSync(); - createFakePubspec(currentExample, name: example, isFlutter: isFlutter); - } - } - - for (List file in withExtraFiles) { - final List newFilePath = [pluginDirectory.path] - ..addAll(file); - final File newFile = - mockFileSystem.file(mockFileSystem.path.joinAll(newFilePath)); - newFile.createSync(recursive: true); - } - - return pluginDirectory; -} - -/// Creates a `pubspec.yaml` file with a flutter dependency. -void createFakePubspec( - Directory parent, { - String name = 'fake_package', - bool isFlutter = true, - bool includeVersion = false, - bool isAndroidPlugin = false, - bool isIosPlugin = false, - bool isWebPlugin = false, - bool isLinuxPlugin = false, - bool isMacOsPlugin = false, - bool isWindowsPlugin = false, -}) { - parent.childFile('pubspec.yaml').createSync(); - String yaml = ''' -name: $name -flutter: - plugin: - platforms: -'''; - if (isAndroidPlugin) { - yaml += ''' - android: - package: io.flutter.plugins.fake - pluginClass: FakePlugin -'''; - } - if (isIosPlugin) { - yaml += ''' - ios: - pluginClass: FLTFakePlugin -'''; - } - if (isWebPlugin) { - yaml += ''' - web: - pluginClass: FakePlugin - fileName: ${name}_web.dart -'''; - } - if (isLinuxPlugin) { - yaml += ''' - linux: - pluginClass: FakePlugin -'''; - } - if (isMacOsPlugin) { - yaml += ''' - macos: - pluginClass: FakePlugin -'''; - } - if (isWindowsPlugin) { - yaml += ''' - windows: - pluginClass: FakePlugin -'''; - } - if (isFlutter) { - yaml += ''' -dependencies: - flutter: - sdk: flutter -'''; - } - if (includeVersion) { - yaml += ''' -version: 0.0.1 -publish_to: none # Hardcoded safeguard to prevent this from somehow being published by a broken test. -'''; - } - parent.childFile('pubspec.yaml').writeAsStringSync(yaml); -} - -/// Cleans up the mock packages directory, making it an empty directory again. -void cleanupPackages() { - mockPackagesDir.listSync().forEach((FileSystemEntity entity) { - entity.deleteSync(recursive: true); - }); -} - -/// Run the command [runner] with the given [args] and return -/// what was printed. -Future> runCapturingPrint( - CommandRunner runner, List args) async { - final List prints = []; - final ZoneSpecification spec = ZoneSpecification( - print: (_, __, ___, String message) { - prints.add(message); - }, - ); - await Zone.current - .fork(specification: spec) - .run>(() => runner.run(args)); - - return prints; -} - -/// A mock [ProcessRunner] which records process calls. -class RecordingProcessRunner extends ProcessRunner { - io.Process processToReturn; - final List recordedCalls = []; - - /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. - String resultStdout; - - /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. - String resultStderr; - - @override - Future runAndStream( - String executable, - List args, { - Directory workingDir, - bool exitOnError = false, - }) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - return Future.value( - processToReturn == null ? 0 : await processToReturn.exitCode); - } - - /// Returns [io.ProcessResult] created from [processToReturn], [resultStdout], and [resultStderr]. - @override - Future run(String executable, List args, - {Directory workingDir, - bool exitOnError = false, - stdoutEncoding = io.systemEncoding, - stderrEncoding = io.systemEncoding}) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - io.ProcessResult result; - - if (processToReturn != null) { - result = io.ProcessResult( - processToReturn.pid, - await processToReturn.exitCode, - resultStdout ?? processToReturn.stdout, - resultStderr ?? processToReturn.stderr); - } - return Future.value(result); - } - - @override - Future runAndExitOnError( - String executable, - List args, { - Directory workingDir, - }) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - io.ProcessResult result; - if (processToReturn != null) { - result = io.ProcessResult( - processToReturn.pid, - await processToReturn.exitCode, - resultStdout ?? processToReturn.stdout, - resultStderr ?? processToReturn.stderr); - } - return Future.value(result); - } - - @override - Future start(String executable, List args, - {Directory workingDirectory}) async { - recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); - return Future.value(processToReturn); - } -} - -/// A recorded process call. -class ProcessCall { - const ProcessCall(this.executable, this.args, this.workingDir); - - /// The executable that was called. - final String executable; - - /// The arguments passed to [executable] in the call. - final List args; - - /// The working directory this process was called from. - final String workingDir; - - @override - bool operator ==(dynamic other) { - if (other is! ProcessCall) { - return false; - } - final ProcessCall otherCall = other; - return executable == otherCall.executable && - listsEqual(args, otherCall.args) && - workingDir == otherCall.workingDir; - } - - @override - int get hashCode => - executable?.hashCode ?? - 0 ^ args?.hashCode ?? - 0 ^ workingDir?.hashCode ?? - 0; - - @override - String toString() { - final List command = [executable]..addAll(args); - return '"${command.join(' ')}" in $workingDir'; - } -} diff --git a/packages/flutter_plugin_tools/test/version_check_test.dart b/packages/flutter_plugin_tools/test/version_check_test.dart deleted file mode 100644 index b9ace3811bff..000000000000 --- a/packages/flutter_plugin_tools/test/version_check_test.dart +++ /dev/null @@ -1,319 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:git/git.dart'; -import 'package:mockito/mockito.dart'; -import "package:test/test.dart"; -import "package:flutter_plugin_tools/src/version_check_command.dart"; -import 'package:pub_semver/pub_semver.dart'; -import 'util.dart'; - -void testAllowedVersion( - String masterVersion, - String headVersion, { - bool allowed = true, - NextVersionType nextVersionType, -}) { - final Version master = Version.parse(masterVersion); - final Version head = Version.parse(headVersion); - final Map allowedVersions = - getAllowedNextVersions(master, head); - if (allowed) { - expect(allowedVersions, contains(head)); - if (nextVersionType != null) { - expect(allowedVersions[head], equals(nextVersionType)); - } - } else { - expect(allowedVersions, isNot(contains(head))); - } -} - -class MockGitDir extends Mock implements GitDir {} - -class MockProcessResult extends Mock implements ProcessResult {} - -void main() { - group('$VersionCheckCommand', () { - CommandRunner runner; - RecordingProcessRunner processRunner; - List> gitDirCommands; - String gitDiffResponse; - Map gitShowResponses; - - setUp(() { - gitDirCommands = >[]; - gitDiffResponse = ''; - gitShowResponses = {}; - final MockGitDir gitDir = MockGitDir(); - when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0]); - final MockProcessResult mockProcessResult = MockProcessResult(); - if (invocation.positionalArguments[0][0] == 'diff') { - when(mockProcessResult.stdout).thenReturn(gitDiffResponse); - } else if (invocation.positionalArguments[0][0] == 'show') { - final String response = - gitShowResponses[invocation.positionalArguments[0][1]]; - when(mockProcessResult.stdout).thenReturn(response); - } - return Future.value(mockProcessResult); - }); - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final VersionCheckCommand command = VersionCheckCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner, gitDir: gitDir); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); - }); - - tearDown(() { - cleanupPackages(); - }); - - test('allows valid version', () async { - createFakePlugin('plugin'); - gitDiffResponse = "packages/plugin/pubspec.yaml"; - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', - }; - final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - - expect( - output, - orderedEquals([ - 'No version check errors found!', - ]), - ); - expect(gitDirCommands.length, equals(3)); - expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect(gitDirCommands[1].join(' '), - equals('show master:packages/plugin/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin/pubspec.yaml')); - }); - - test('denies invalid version', () async { - createFakePlugin('plugin'); - gitDiffResponse = "packages/plugin/pubspec.yaml"; - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', - }; - final Future> result = runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - - await expectLater( - result, - throwsA(const TypeMatcher()), - ); - expect(gitDirCommands.length, equals(3)); - expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect(gitDirCommands[1].join(' '), - equals('show master:packages/plugin/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin/pubspec.yaml')); - }); - - test('gracefully handles missing pubspec.yaml', () async { - createFakePlugin('plugin'); - gitDiffResponse = "packages/plugin/pubspec.yaml"; - mockFileSystem.currentDirectory - .childDirectory('packages') - .childDirectory('plugin') - .childFile('pubspec.yaml') - .deleteSync(); - final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - - expect( - output, - orderedEquals([ - 'No version check errors found!', - ]), - ); - expect(gitDirCommands.length, equals(1)); - expect(gitDirCommands.first.join(' '), - equals('diff --name-only master HEAD')); - }); - - test('allows minor changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface'); - gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; - gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.0.0', - 'HEAD:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.1.0', - }; - final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - expect( - output, - orderedEquals([ - 'No version check errors found!', - ]), - ); - expect(gitDirCommands.length, equals(3)); - expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect( - gitDirCommands[1].join(' '), - equals( - 'show master:packages/plugin_platform_interface/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); - }); - - test('disallows breaking changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface'); - gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; - gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.0.0', - 'HEAD:packages/plugin_platform_interface/pubspec.yaml': - 'version: 2.0.0', - }; - final Future> output = runCapturingPrint( - runner, ['version-check', '--base_sha=master']); - await expectLater( - output, - throwsA(const TypeMatcher()), - ); - expect(gitDirCommands.length, equals(3)); - expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect( - gitDirCommands[1].join(' '), - equals( - 'show master:packages/plugin_platform_interface/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); - }); - }); - - group("Pre 1.0", () { - test("nextVersion allows patch version", () { - testAllowedVersion("0.12.0", "0.12.0+1", - nextVersionType: NextVersionType.PATCH); - testAllowedVersion("0.12.0+4", "0.12.0+5", - nextVersionType: NextVersionType.PATCH); - }); - - test("nextVersion does not allow jumping patch", () { - testAllowedVersion("0.12.0", "0.12.0+2", allowed: false); - testAllowedVersion("0.12.0+2", "0.12.0+4", allowed: false); - }); - - test("nextVersion does not allow going back", () { - testAllowedVersion("0.12.0", "0.11.0", allowed: false); - testAllowedVersion("0.12.0+2", "0.12.0+1", allowed: false); - testAllowedVersion("0.12.0+1", "0.12.0", allowed: false); - }); - - test("nextVersion allows minor version", () { - testAllowedVersion("0.12.0", "0.12.1", - nextVersionType: NextVersionType.MINOR); - testAllowedVersion("0.12.0+4", "0.12.1", - nextVersionType: NextVersionType.MINOR); - }); - - test("nextVersion does not allow jumping minor", () { - testAllowedVersion("0.12.0", "0.12.2", allowed: false); - testAllowedVersion("0.12.0+2", "0.12.3", allowed: false); - }); - }); - - group("Releasing 1.0", () { - test("nextVersion allows releasing 1.0", () { - testAllowedVersion("0.12.0", "1.0.0", - nextVersionType: NextVersionType.BREAKING_MAJOR); - testAllowedVersion("0.12.0+4", "1.0.0", - nextVersionType: NextVersionType.BREAKING_MAJOR); - }); - - test("nextVersion does not allow jumping major", () { - testAllowedVersion("0.12.0", "2.0.0", allowed: false); - testAllowedVersion("0.12.0+4", "2.0.0", allowed: false); - }); - - test("nextVersion does not allow un-releasing", () { - testAllowedVersion("1.0.0", "0.12.0+4", allowed: false); - testAllowedVersion("1.0.0", "0.12.0", allowed: false); - }); - }); - - group("Post 1.0", () { - test("nextVersion allows patch jumps", () { - testAllowedVersion("1.0.1", "1.0.2", - nextVersionType: NextVersionType.PATCH); - testAllowedVersion("1.0.0", "1.0.1", - nextVersionType: NextVersionType.PATCH); - }); - - test("nextVersion does not allow build jumps", () { - testAllowedVersion("1.0.1", "1.0.1+1", allowed: false); - testAllowedVersion("1.0.0+5", "1.0.0+6", allowed: false); - }); - - test("nextVersion does not allow skipping patches", () { - testAllowedVersion("1.0.1", "1.0.3", allowed: false); - testAllowedVersion("1.0.0", "1.0.6", allowed: false); - }); - - test("nextVersion allows minor version jumps", () { - testAllowedVersion("1.0.1", "1.1.0", - nextVersionType: NextVersionType.MINOR); - testAllowedVersion("1.0.0", "1.1.0", - nextVersionType: NextVersionType.MINOR); - }); - - test("nextVersion does not allow skipping minor versions", () { - testAllowedVersion("1.0.1", "1.2.0", allowed: false); - testAllowedVersion("1.1.0", "1.3.0", allowed: false); - }); - - test("nextVersion allows breaking changes", () { - testAllowedVersion("1.0.1", "2.0.0", - nextVersionType: NextVersionType.BREAKING_MAJOR); - testAllowedVersion("1.0.0", "2.0.0", - nextVersionType: NextVersionType.BREAKING_MAJOR); - }); - - test("nextVersion allows null safety pre prelease", () { - testAllowedVersion("1.0.1", "2.0.0-nullsafety", - nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0", "2.0.0-nullsafety", - nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0-nullsafety", "1.0.0-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0-nullsafety.1", "1.0.0-nullsafety.2", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0", "0.2.0-nullsafety", - nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0-nullsafety", "0.1.0-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0-nullsafety.1", "0.1.0-nullsafety.2", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0", "1.1.0-nullsafety", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.1.0-nullsafety", "1.1.0-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0", "0.1.1-nullsafety", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.1-nullsafety", "0.1.1-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - }); - - test("nextVersion does not allow skipping major versions", () { - testAllowedVersion("1.0.1", "3.0.0", allowed: false); - testAllowedVersion("1.1.0", "2.3.0", allowed: false); - }); - }); -} diff --git a/packages/flutter_plugin_tools/test/xctest_command_test.dart b/packages/flutter_plugin_tools/test/xctest_command_test.dart deleted file mode 100644 index 007c2e12188c..000000000000 --- a/packages/flutter_plugin_tools/test/xctest_command_test.dart +++ /dev/null @@ -1,358 +0,0 @@ -// 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 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/xctest_command.dart'; -import 'package:test/test.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -final _kDeviceListMap = { - "runtimes": [ - { - "bundlePath": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime", - "buildversion": "17A577", - "runtimeRoot": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot", - "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-0", - "version": "13.0", - "isAvailable": true, - "name": "iOS 13.0" - }, - { - "bundlePath": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime", - "buildversion": "17L255", - "runtimeRoot": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot", - "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-4", - "version": "13.4", - "isAvailable": true, - "name": "iOS 13.4" - }, - { - "bundlePath": - "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime", - "buildversion": "17T531", - "runtimeRoot": - "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot", - "identifier": "com.apple.CoreSimulator.SimRuntime.watchOS-6-2", - "version": "6.2.1", - "isAvailable": true, - "name": "watchOS 6.2" - } - ], - "devices": { - "com.apple.CoreSimulator.SimRuntime.iOS-13-4": [ - { - "dataPath": - "/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data", - "logPath": - "/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774", - "udid": "2706BBEB-1E01-403E-A8E9-70E8E5A24774", - "isAvailable": true, - "deviceTypeIdentifier": - "com.apple.CoreSimulator.SimDeviceType.iPhone-8", - "state": "Shutdown", - "name": "iPhone 8" - }, - { - "dataPath": - "/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data", - "logPath": - "/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A", - "udid": "1E76A0FD-38AC-4537-A989-EA639D7D012A", - "isAvailable": true, - "deviceTypeIdentifier": - "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus", - "state": "Shutdown", - "name": "iPhone 8 Plus" - } - ] - } -}; - -void main() { - const String _kDestination = '--ios-destination'; - const String _kTarget = '--target'; - const String _kSkip = '--skip'; - - group('test xctest_command', () { - CommandRunner runner; - RecordingProcessRunner processRunner; - - setUp(() { - initializeFakePackages(); - processRunner = RecordingProcessRunner(); - final XCTestCommand command = XCTestCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); - - runner = CommandRunner('xctest_command', 'Test for xctest_command'); - runner.addCommand(command); - cleanupPackages(); - }); - - test('Not specifying --target throws', () async { - await expectLater( - () => runner.run(['xctest', _kDestination, 'a_destination']), - throwsA(const TypeMatcher())); - }); - - test('skip if ios is not supported', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: false); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - final List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination' - ]); - expect(output, contains('iOS is not supported by this plugin.')); - expect(processRunner.recordedCalls, orderedEquals([])); - - cleanupPackages(); - }); - - test('running with correct scheme and destination, did not find scheme', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - processRunner.resultStdout = '{"project":{"targets":["bar_scheme"]}}'; - - await expectLater(() async { - final List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination' - ]); - expect(output, - contains('foo_scheme not configured for plugin, test failed.')); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('xcrun', ['simctl', 'list', '--json'], null), - ProcessCall( - 'xcodebuild', - [ - '-project', - 'ios/Runner.xcodeproj', - '-list', - '-json' - ], - pluginExampleDirectory.path), - ])); - }, throwsA(const TypeMatcher())); - cleanupPackages(); - }); - - test('running with correct scheme and destination, found scheme', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; - List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination' - ]); - - expect(output, contains('Successfully ran xctest for plugin')); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcodebuild', - ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], - pluginExampleDirectory.path), - ProcessCall( - 'xcodebuild', - [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'foo_scheme', - '-destination', - 'foo_destination', - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ], - pluginExampleDirectory.path), - ])); - - cleanupPackages(); - }); - - test('running with correct scheme and destination, skip 1 plugin', - () async { - createFakePlugin('plugin1', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - createFakePlugin('plugin2', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory1 = - mockPackagesDir.childDirectory('plugin1').childDirectory('example'); - createFakePubspec(pluginExampleDirectory1, isFlutter: true); - final Directory pluginExampleDirectory2 = - mockPackagesDir.childDirectory('plugin2').childDirectory('example'); - createFakePubspec(pluginExampleDirectory2, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; - List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination', - _kSkip, - 'plugin1' - ]); - - expect(output, contains('plugin1 was skipped with the --skip flag.')); - expect(output, contains('Successfully ran xctest for plugin2')); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcodebuild', - ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], - pluginExampleDirectory2.path), - ProcessCall( - 'xcodebuild', - [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'foo_scheme', - '-destination', - 'foo_destination', - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ], - pluginExampleDirectory2.path), - ])); - - cleanupPackages(); - }); - - test('Not specifying --ios-destination assigns an available simulator', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - final Map schemeCommandResult = { - "project": { - "targets": ["bar_scheme", "foo_scheme"] - } - }; - // For simplicity of the test, we combine all the mock results into a single mock result, each internal command - // will get this result and they should still be able to parse them correctly. - processRunner.resultStdout = - jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); - await runner.run([ - 'xctest', - _kTarget, - 'foo_scheme', - ]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('xcrun', ['simctl', 'list', '--json'], null), - ProcessCall( - 'xcodebuild', - ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], - pluginExampleDirectory.path), - ProcessCall( - 'xcodebuild', - [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'foo_scheme', - '-destination', - 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ], - pluginExampleDirectory.path), - ])); - - cleanupPackages(); - }); - }); -} From 1662be69140cfc7594bdfdfe6cc3a17e39e6c192 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 28 Jan 2021 11:56:58 -0800 Subject: [PATCH 26/27] revert test code --- .../camera/test/camera_image_stream_test.dart | 2 +- packages/espresso/example/lib/main.dart | 2 +- script/incremental_build.sh | 26 +++++++++---------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/camera/camera/test/camera_image_stream_test.dart b/packages/camera/camera/test/camera_image_stream_test.dart index 2d339c454144..57e3aeb36f3f 100644 --- a/packages/camera/camera/test/camera_image_stream_test.dart +++ b/packages/camera/camera/test/camera_image_stream_test.dart @@ -13,7 +13,7 @@ void main() { CameraPlatform.instance = MockCameraPlatform(); }); - test('startImageStream() throws $CameraException when uninitialized 1', () { + test('startImageStream() throws $CameraException when uninitialized', () { CameraController cameraController = CameraController( CameraDescription( name: 'cam', diff --git a/packages/espresso/example/lib/main.dart b/packages/espresso/example/lib/main.dart index 72bfbe1f26d8..c74423f507e8 100644 --- a/packages/espresso/example/lib/main.dart +++ b/packages/espresso/example/lib/main.dart @@ -8,7 +8,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo1', + title: 'Flutter Demo', theme: ThemeData( // This is the theme of your application. // diff --git a/script/incremental_build.sh b/script/incremental_build.sh index 4b814a006e4e..497451070c50 100755 --- a/script/incremental_build.sh +++ b/script/incremental_build.sh @@ -52,18 +52,16 @@ if [[ "${BRANCH_NAME}" == "master" ]]; then (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) else # Sets CHANGED_PACKAGES - # check_changed_packages - (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) - - # if [[ "$CHANGED_PACKAGES" == "" ]]; then - # echo "No changes detected in packages." - # echo "Running for all packages" - # (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) - # else - # echo running "${ACTIONS[@]}" - # (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --plugins="$CHANGED_PACKAGES" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) - # echo "Running version check for changed packages" - # # TODO(egarciad): Enable this check once in master. - # # (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools version-check --base_sha="$(get_branch_base_sha)") - # fi + check_changed_packages + if [[ "$CHANGED_PACKAGES" == "" ]]; then + echo "No changes detected in packages." + echo "Running for all packages" + (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) + else + echo running "${ACTIONS[@]}" + (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --plugins="$CHANGED_PACKAGES" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) + echo "Running version check for changed packages" + # TODO(egarciad): Enable this check once in master. + # (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools version-check --base_sha="$(get_branch_base_sha)") + fi fi From 022e0d6ac53dad51e209a313d0d5cdb7a9110998 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 28 Jan 2021 11:59:53 -0800 Subject: [PATCH 27/27] revert --- script/incremental_build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/script/incremental_build.sh b/script/incremental_build.sh index 497451070c50..3911f0a6e9c8 100755 --- a/script/incremental_build.sh +++ b/script/incremental_build.sh @@ -53,6 +53,7 @@ if [[ "${BRANCH_NAME}" == "master" ]]; then else # Sets CHANGED_PACKAGES check_changed_packages + if [[ "$CHANGED_PACKAGES" == "" ]]; then echo "No changes detected in packages." echo "Running for all packages"