diff --git a/.ci.yaml b/.ci.yaml index 50b49bdaf3c..b16bad0dfc9 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 df9554a4fd1..39c29b32389 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 00000000000..c585ef2cb0c --- /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 d95484bebbc..21cbd824c5d 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 eeb90783e16..5e2a3058fae 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 f2f54f47394..79f6b75e7a6 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 fa403d52d59..c58caa5dc63 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 00000000000..d19e6614d83 --- /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 d1771be8671..d4b45206c2d 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 f9f7f2d348f..0674b56e623 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 = '../..';