From e3c51a2f2a3352415069c61f732b88ffc08ba306 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 24 Jan 2023 10:23:57 -0800 Subject: [PATCH] Add Windows unit tests to plugin template (#118638) * Add Windows unit tests to plugin template Adds an example native unit test to the plugin template for Windows, matching the format we use for our 1P plugin example app unit tests. Once these have been added for all platforms+languages, they will be documented on a new plugin development page to explain their use. Since we don't appear to be running our current plugin e2e tests for Windows, this adds a new configuration to run them. I haven't `led`-tested this, so it may not work, but this will give a starting point for getting them running. Part of https://github.com/flutter/flutter/issues/82458 * Minor fix * Add test owner * Fix typo * Fix test feature flag --- .ci.yaml | 18 ++++++++ TESTOWNERS | 1 + .../bin/tasks/plugin_test_windows.dart | 16 +++++++ dev/devicelab/lib/tasks/plugin_tests.dart | 8 ++++ .../windows.tmpl/CMakeLists.txt.tmpl | 5 +++ .../plugin/windows.tmpl/CMakeLists.txt.tmpl | 43 +++++++++++++++++++ .../windows.tmpl/pluginClassSnakeCase.h.tmpl | 1 - .../test/pluginClassSnakeCase_test.cpp.tmpl | 43 +++++++++++++++++++ .../templates/template_manifest.json | 1 + .../commands.shard/permeable/create_test.dart | 26 ++++++++++- 10 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 dev/devicelab/bin/tasks/plugin_test_windows.dart create mode 100644 packages/flutter_tools/templates/plugin/windows.tmpl/test/pluginClassSnakeCase_test.cpp.tmpl diff --git a/.ci.yaml b/.ci.yaml index 50b49bdaf3c4..b16bad0dfc9c 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -4364,6 +4364,24 @@ targets: - bin/** - .ci.yaml + - name: Windows plugin_test_windows + bringup: true # New task + recipe: devicelab/devicelab_drone + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"} + ] + tags: > + ["devicelab", "hostonly", "windows"] + task_name: plugin_test_windows + runIf: + - dev/** + - packages/flutter_tools/** + - bin/** + - .ci.yaml + - name: Windows run_debug_test_windows recipe: devicelab/devicelab_drone presubmit: false diff --git a/TESTOWNERS b/TESTOWNERS index df9554a4fd1c..39c29b323894 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -251,6 +251,7 @@ /dev/devicelab/bin/tasks/plugin_lint_mac.dart @stuartmorgan @flutter/plugin /dev/devicelab/bin/tasks/plugin_test_ios.dart @jmagman @flutter/ios /dev/devicelab/bin/tasks/plugin_test_macos.dart @jmagman @flutter/desktop +/dev/devicelab/bin/tasks/plugin_test_windows.dart @stuartmorgan @flutter/desktop /dev/devicelab/bin/tasks/plugin_test.dart @stuartmorgan @flutter/plugin /dev/devicelab/bin/tasks/run_debug_test_android.dart @zanderso @flutter/tool /dev/devicelab/bin/tasks/run_debug_test_linux.dart @loic-sharma @flutter/tool diff --git a/dev/devicelab/bin/tasks/plugin_test_windows.dart b/dev/devicelab/bin/tasks/plugin_test_windows.dart new file mode 100644 index 000000000000..c585ef2cb0c3 --- /dev/null +++ b/dev/devicelab/bin/tasks/plugin_test_windows.dart @@ -0,0 +1,16 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/plugin_tests.dart'; + +Future main() async { + await task(combine([ + PluginTest('windows', ['--platforms=windows']).call, + // Test that Dart-only plugins are supported. + PluginTest('windows', ['--platforms=windows'], dartOnlyPlugin: true).call, + // Test that FFI plugins are supported. + PluginTest('windows', ['--platforms=windows'], template: 'plugin_ffi').call, + ])); +} diff --git a/dev/devicelab/lib/tasks/plugin_tests.dart b/dev/devicelab/lib/tasks/plugin_tests.dart index d95484bebbca..21cbd824c5d0 100644 --- a/dev/devicelab/lib/tasks/plugin_tests.dart +++ b/dev/devicelab/lib/tasks/plugin_tests.dart @@ -274,6 +274,14 @@ public class $pluginClass: NSObject, FlutterPlugin { )) { throw TaskResult.failure('Platform unit tests failed'); } + } else if (buildTarget == 'windows') { + if (await exec( + path.join(rootPath, 'build', 'windows', 'plugins', 'plugintest', 'Release', 'plugintest_plugin_test'), + [], + canFail: true, + ) != 0) { + throw TaskResult.failure('Platform unit tests failed'); + } } } diff --git a/packages/flutter_tools/templates/app_shared/windows.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/app_shared/windows.tmpl/CMakeLists.txt.tmpl index eeb90783e169..5e2a3058fae9 100644 --- a/packages/flutter_tools/templates/app_shared/windows.tmpl/CMakeLists.txt.tmpl +++ b/packages/flutter_tools/templates/app_shared/windows.tmpl/CMakeLists.txt.tmpl @@ -52,6 +52,11 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") +{{#withPlatformChannelPluginHook}} +# Enable the test target. +set(include_{{pluginProjectName}}_tests TRUE) +{{/withPlatformChannelPluginHook}} + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) diff --git a/packages/flutter_tools/templates/plugin/windows.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/plugin/windows.tmpl/CMakeLists.txt.tmpl index f2f54f47394f..79f6b75e7a6c 100644 --- a/packages/flutter_tools/templates/plugin/windows.tmpl/CMakeLists.txt.tmpl +++ b/packages/flutter_tools/templates/plugin/windows.tmpl/CMakeLists.txt.tmpl @@ -51,3 +51,46 @@ set({{projectName}}_bundled_libraries "" PARENT_SCOPE ) + +# === Tests === +# These unit tests can be run from a terminal after building the example, or +# from Visual Studio after opening the generated solution file. + +# Only enable test builds when building the example (which sets this variable) +# so that plugin clients aren't building the tests. +if (${include_${PROJECT_NAME}_tests}) +set(TEST_RUNNER "${PROJECT_NAME}_test") +enable_testing() + +# Add the Google Test dependency. +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/release-1.11.0.zip +) +# Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# Disable install commands for gtest so it doesn't end up in the bundle. +set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) +FetchContent_MakeAvailable(googletest) + +# The plugin's C API is not very useful for unit testing, so build the sources +# directly into the test binary rather than using the DLL. +add_executable(${TEST_RUNNER} + test/{{pluginClassSnakeCase}}_test.cpp + ${PLUGIN_SOURCES} +) +apply_standard_settings(${TEST_RUNNER}) +target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin) +target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) +# flutter_wrapper_plugin has link dependencies on the Flutter DLL. +add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${FLUTTER_LIBRARY}" $ +) + +# Enable automatic test discovery. +include(GoogleTest) +gtest_discover_tests(${TEST_RUNNER}) +endif() diff --git a/packages/flutter_tools/templates/plugin/windows.tmpl/pluginClassSnakeCase.h.tmpl b/packages/flutter_tools/templates/plugin/windows.tmpl/pluginClassSnakeCase.h.tmpl index fa403d52d599..c58caa5dc638 100644 --- a/packages/flutter_tools/templates/plugin/windows.tmpl/pluginClassSnakeCase.h.tmpl +++ b/packages/flutter_tools/templates/plugin/windows.tmpl/pluginClassSnakeCase.h.tmpl @@ -20,7 +20,6 @@ class {{pluginClass}} : public flutter::Plugin { {{pluginClass}}(const {{pluginClass}}&) = delete; {{pluginClass}}& operator=(const {{pluginClass}}&) = delete; - private: // Called when a method is called on this plugin's channel from Dart. void HandleMethodCall( const flutter::MethodCall &method_call, diff --git a/packages/flutter_tools/templates/plugin/windows.tmpl/test/pluginClassSnakeCase_test.cpp.tmpl b/packages/flutter_tools/templates/plugin/windows.tmpl/test/pluginClassSnakeCase_test.cpp.tmpl new file mode 100644 index 000000000000..d19e6614d831 --- /dev/null +++ b/packages/flutter_tools/templates/plugin/windows.tmpl/test/pluginClassSnakeCase_test.cpp.tmpl @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "{{pluginClassSnakeCase}}.h" + +namespace {{projectName}} { +namespace test { + +namespace { + +using flutter::EncodableMap; +using flutter::EncodableValue; +using flutter::MethodCall; +using flutter::MethodResultFunctions; + +} // namespace + +TEST({{pluginClass}}, GetPlatformVersion) { + {{pluginClass}} plugin; + // Save the reply value from the success callback. + std::string result_string; + plugin.HandleMethodCall( + MethodCall("getPlatformVersion", std::make_unique()), + std::make_unique>( + [&result_string](const EncodableValue* result) { + result_string = std::get(*result); + }, + nullptr, nullptr)); + + // Since the exact string varies by host, just ensure that it's a string + // with the expected format. + EXPECT_TRUE(result_string.rfind("Windows ", 0) == 0); +} + +} // namespace test +} // namespace {{projectName}} diff --git a/packages/flutter_tools/templates/template_manifest.json b/packages/flutter_tools/templates/template_manifest.json index d1771be8671b..d4b45206c2d8 100644 --- a/packages/flutter_tools/templates/template_manifest.json +++ b/packages/flutter_tools/templates/template_manifest.json @@ -278,6 +278,7 @@ "templates/plugin/test/projectName_method_channel_test.dart.tmpl", "templates/plugin/windows.tmpl/CMakeLists.txt.tmpl", "templates/plugin/windows.tmpl/include/projectName.tmpl/pluginClassSnakeCase_c_api.h.tmpl", + "templates/plugin/windows.tmpl/test/pluginClassSnakeCase_test.cpp.tmpl", "templates/plugin/windows.tmpl/pluginClassSnakeCase.cpp.tmpl", "templates/plugin/windows.tmpl/pluginClassSnakeCase.h.tmpl", "templates/plugin/windows.tmpl/pluginClassSnakeCase_c_api.cpp.tmpl", diff --git a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart index f9f7f2d348f7..0674b56e6230 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart @@ -2429,7 +2429,7 @@ void main() { androidIdentifier: 'com.example.flutter_project'); }); - testUsingContext('plugin includes native unit tests', () async { + testUsingContext('plugin includes native Swift unit tests', () async { Cache.flutterRoot = '../..'; final CreateCommand command = CreateCommand(); @@ -2452,7 +2452,7 @@ void main() { Logger: () => logger, }); - testUsingContext('plugin includes native Ojb-C unit tests', () async { + testUsingContext('plugin includes native Objective-C unit tests', () async { Cache.flutterRoot = '../..'; final CreateCommand command = CreateCommand(); @@ -2476,6 +2476,28 @@ void main() { Logger: () => logger, }); + testUsingContext('plugin includes native Windows unit tests', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); + + await runner.run([ + 'create', + '--no-pub', + '--template=plugin', + '--platforms=windows', + projectDir.path]); + + expect(projectDir + .childDirectory('windows') + .childDirectory('test') + .childFile('flutter_project_plugin_test.cpp'), exists); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true), + Logger: () => logger, + }); + testUsingContext('create a module with --platforms throws error.', () async { Cache.flutterRoot = '../..';