From e65dfba8e1b1b7a2a79802ce4c78e818a501ddd6 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 15 Feb 2023 11:13:11 -0800 Subject: [PATCH] Add Linux unit tests to plugin template (#120814) * Add Linux unit tests to plugin template Adds an example native unit test to the plugin template for Linux, matching the structure we use for our 1P plugin 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. While ideally we would adjust the engine APIs first to allow for testing the method call handler directly, it's unclear when we will have time for that work, and for a complex plugin most of the testing wouldn't be at that layer anyway, so having the structure in place with the limitations documented is still a significant improvement over having nothing in the template. Part of https://github.com/flutter/flutter/issues/82458 * Add creation test * Add integration tests * Missing newlines * test owner * Typo --- .ci.yaml | 20 +++++++ TESTOWNERS | 1 + .../bin/tasks/plugin_test_linux.dart | 16 ++++++ dev/devicelab/lib/tasks/plugin_tests.dart | 9 ++++ .../app_shared/linux.tmpl/CMakeLists.txt.tmpl | 5 ++ .../plugin/linux.tmpl/CMakeLists.txt.tmpl | 53 +++++++++++++++++-- .../linux.tmpl/pluginClassSnakeCase.cc.tmpl | 16 ++++-- .../pluginClassSnakeCase_private.h.tmpl | 10 ++++ .../test/pluginClassSnakeCase_test.cc.tmpl | 31 +++++++++++ .../templates/template_manifest.json | 2 + .../commands.shard/permeable/create_test.dart | 22 ++++++++ 11 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 dev/devicelab/bin/tasks/plugin_test_linux.dart create mode 100644 packages/flutter_tools/templates/plugin/linux.tmpl/pluginClassSnakeCase_private.h.tmpl create mode 100644 packages/flutter_tools/templates/plugin/linux.tmpl/test/pluginClassSnakeCase_test.cc.tmpl diff --git a/.ci.yaml b/.ci.yaml index b664b7836e..5c1b04dd7b 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -697,6 +697,26 @@ targets: - bin/** - .ci.yaml + - name: Linux plugin_test_linux + recipe: devicelab/devicelab_drone + bringup: true # New task + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, + {"dependency": "cmake", "version": "version:3.16.1"}, + {"dependency": "ninja", "version": "version:1.9.0"} + ] + tags: > + ["devicelab", "hostonly", "linux"] + task_name: plugin_test_linux + runIf: + - dev/** + - packages/flutter_tools/** + - bin/** + - .ci.yaml + - name: Linux run_debug_test_linux recipe: devicelab/devicelab_drone bringup: true diff --git a/TESTOWNERS b/TESTOWNERS index 32627ce013..dac94ada50 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -252,6 +252,7 @@ /dev/devicelab/bin/tasks/platform_view_win_desktop__start_up.dart @yaakovschectman @flutter/desktop /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_linux.dart @stuartmorgan @flutter/desktop /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 diff --git a/dev/devicelab/bin/tasks/plugin_test_linux.dart b/dev/devicelab/bin/tasks/plugin_test_linux.dart new file mode 100644 index 0000000000..8e2a9e2909 --- /dev/null +++ b/dev/devicelab/bin/tasks/plugin_test_linux.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('linux', ['--platforms=linux']).call, + // Test that Dart-only plugins are supported. + PluginTest('linux', ['--platforms=linux'], dartOnlyPlugin: true).call, + // Test that FFI plugins are supported. + PluginTest('linux', ['--platforms=linux'], template: 'plugin_ffi').call, + ])); +} diff --git a/dev/devicelab/lib/tasks/plugin_tests.dart b/dev/devicelab/lib/tasks/plugin_tests.dart index 16d9d55986..c0e2854a3b 100644 --- a/dev/devicelab/lib/tasks/plugin_tests.dart +++ b/dev/devicelab/lib/tasks/plugin_tests.dart @@ -276,6 +276,15 @@ public class $pluginClass: NSObject, FlutterPlugin { } }); break; + case 'linux': + if (await exec( + path.join(rootPath, 'build', 'linux', 'x64', 'release', 'plugins', 'plugintest', 'plugintest_plugin_test'), + [], + canFail: true, + ) != 0) { + throw TaskResult.failure('Platform unit tests failed'); + } + break; case 'macos': if (!await runXcodeTests( platformDirectory: path.join(rootPath, 'macos'), diff --git a/packages/flutter_tools/templates/app_shared/linux.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/app_shared/linux.tmpl/CMakeLists.txt.tmpl index f2269110c9..a4fc907bec 100644 --- a/packages/flutter_tools/templates/app_shared/linux.tmpl/CMakeLists.txt.tmpl +++ b/packages/flutter_tools/templates/app_shared/linux.tmpl/CMakeLists.txt.tmpl @@ -86,6 +86,11 @@ set_target_properties(${BINARY_NAME} RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" ) +{{#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/linux.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/plugin/linux.tmpl/CMakeLists.txt.tmpl index 40a885d92f..2d8110fe75 100644 --- a/packages/flutter_tools/templates/plugin/linux.tmpl/CMakeLists.txt.tmpl +++ b/packages/flutter_tools/templates/plugin/linux.tmpl/CMakeLists.txt.tmpl @@ -11,12 +11,15 @@ project(${PROJECT_NAME} LANGUAGES CXX) # not be changed. set(PLUGIN_NAME "{{projectName}}_plugin") +# Any new source files that you add to the plugin should be added here. +list(APPEND PLUGIN_SOURCES + "{{pluginClassSnakeCase}}.cc" +) + # Define the plugin library target. Its name must not be changed (see comment # on PLUGIN_NAME above). -# -# Any new source files that you add to the plugin should be added here. add_library(${PLUGIN_NAME} SHARED - "{{pluginClassSnakeCase}}.cc" + ${PLUGIN_SOURCES} ) # Apply a standard set of build settings that are configured in the @@ -45,3 +48,47 @@ set({{projectName}}_bundled_libraries "" PARENT_SCOPE ) + +# === Tests === +# These unit tests can be run from a terminal after building the example. + +# 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}) +if(${CMAKE_VERSION} VERSION_LESS "3.11.0") +message("Unit tests require CMake 3.11.0 or later") +else() +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 exported API is not very useful for unit testing, so build the +# sources directly into the test binary rather than using the shared library. +add_executable(${TEST_RUNNER} + test/{{pluginClassSnakeCase}}_test.cc + ${PLUGIN_SOURCES} +) +apply_standard_settings(${TEST_RUNNER}) +target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(${TEST_RUNNER} PRIVATE flutter) +target_link_libraries(${TEST_RUNNER} PRIVATE PkgConfig::GTK) +target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) + +# Enable automatic test discovery. +include(GoogleTest) +gtest_discover_tests(${TEST_RUNNER}) + +endif() # CMake version check +endif() # include_${PROJECT_NAME}_tests \ No newline at end of file diff --git a/packages/flutter_tools/templates/plugin/linux.tmpl/pluginClassSnakeCase.cc.tmpl b/packages/flutter_tools/templates/plugin/linux.tmpl/pluginClassSnakeCase.cc.tmpl index 4fa617a9ca..4eeb03c757 100644 --- a/packages/flutter_tools/templates/plugin/linux.tmpl/pluginClassSnakeCase.cc.tmpl +++ b/packages/flutter_tools/templates/plugin/linux.tmpl/pluginClassSnakeCase.cc.tmpl @@ -6,6 +6,8 @@ #include +#include "{{pluginClassSnakeCase}}_private.h" + #define {{pluginClassCapitalSnakeCase}}(obj) \ (G_TYPE_CHECK_INSTANCE_CAST((obj), {{pluginClassSnakeCase}}_get_type(), \ {{pluginClass}})) @@ -25,11 +27,7 @@ static void {{pluginClassSnakeCase}}_handle_method_call( const gchar* method = fl_method_call_get_name(method_call); if (strcmp(method, "getPlatformVersion") == 0) { - struct utsname uname_data = {}; - uname(&uname_data); - g_autofree gchar *version = g_strdup_printf("Linux %s", uname_data.version); - g_autoptr(FlValue) result = fl_value_new_string(version); - response = FL_METHOD_RESPONSE(fl_method_success_response_new(result)); + response = get_platform_version(); } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); } @@ -37,6 +35,14 @@ static void {{pluginClassSnakeCase}}_handle_method_call( fl_method_call_respond(method_call, response, nullptr); } +FlMethodResponse* get_platform_version() { + struct utsname uname_data = {}; + uname(&uname_data); + g_autofree gchar *version = g_strdup_printf("Linux %s", uname_data.version); + g_autoptr(FlValue) result = fl_value_new_string(version); + return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); +} + static void {{pluginClassSnakeCase}}_dispose(GObject* object) { G_OBJECT_CLASS({{pluginClassSnakeCase}}_parent_class)->dispose(object); } diff --git a/packages/flutter_tools/templates/plugin/linux.tmpl/pluginClassSnakeCase_private.h.tmpl b/packages/flutter_tools/templates/plugin/linux.tmpl/pluginClassSnakeCase_private.h.tmpl new file mode 100644 index 0000000000..604a14e30e --- /dev/null +++ b/packages/flutter_tools/templates/plugin/linux.tmpl/pluginClassSnakeCase_private.h.tmpl @@ -0,0 +1,10 @@ +#include + +#include "include/{{projectName}}/{{pluginClassSnakeCase}}.h" + +// This file exposes some plugin internals for unit testing. See +// https://github.com/flutter/flutter/issues/88724 for current limitations +// in the unit-testable API. + +// Handles the getPlatformVersion method call. +FlMethodResponse *get_platform_version(); diff --git a/packages/flutter_tools/templates/plugin/linux.tmpl/test/pluginClassSnakeCase_test.cc.tmpl b/packages/flutter_tools/templates/plugin/linux.tmpl/test/pluginClassSnakeCase_test.cc.tmpl new file mode 100644 index 0000000000..c5bfdde820 --- /dev/null +++ b/packages/flutter_tools/templates/plugin/linux.tmpl/test/pluginClassSnakeCase_test.cc.tmpl @@ -0,0 +1,31 @@ +#include +#include +#include + +#include "include/{{projectName}}/{{pluginClassSnakeCase}}.h" +#include "{{pluginClassSnakeCase}}_private.h" + +// This demonstrates a simple unit test of the C portion of this plugin's +// implementation. +// +// Once you have built the plugin's example app, you can run these tests +// from the command line. For instance, for a plugin called my_plugin +// built for x64 debug, run: +// $ build/linux/x64/debug/plugins/my_plugin/my_plugin_test + +namespace {{projectName}} { +namespace test { + +TEST({{pluginClass}}, GetPlatformVersion) { + g_autoptr(FlMethodResponse) response = get_platform_version(); + ASSERT_NE(response, nullptr); + ASSERT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + FlValue* result = fl_method_success_response_get_result( + FL_METHOD_SUCCESS_RESPONSE(response)); + ASSERT_EQ(fl_value_get_type(result), FL_VALUE_TYPE_STRING); + // The full string varies, so just valiate that it has the right format. + EXPECT_THAT(fl_value_get_string(result), testing::StartsWith("Linux ")); +} + +} // namespace test +} // namespace {{projectName}} diff --git a/packages/flutter_tools/templates/template_manifest.json b/packages/flutter_tools/templates/template_manifest.json index 2ba43bc268..d0902d5edc 100644 --- a/packages/flutter_tools/templates/template_manifest.json +++ b/packages/flutter_tools/templates/template_manifest.json @@ -274,6 +274,8 @@ "templates/plugin/linux.tmpl/CMakeLists.txt.tmpl", "templates/plugin/linux.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl", "templates/plugin/linux.tmpl/pluginClassSnakeCase.cc.tmpl", + "templates/plugin/linux.tmpl/pluginClassSnakeCase_private.h.tmpl", + "templates/plugin/linux.tmpl/test/pluginClassSnakeCase_test.cc.tmpl", "templates/plugin/macos.tmpl/Classes/pluginClass.swift.tmpl", "templates/plugin/README.md.tmpl", "templates/plugin/test/projectName_test.dart.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 0f285de84f..b1c802a890 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart @@ -2555,6 +2555,28 @@ void main() { Logger: () => logger, }); + testUsingContext('plugin includes native Linux unit tests', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); + + await runner.run([ + 'create', + '--no-pub', + '--template=plugin', + '--platforms=linux', + projectDir.path]); + + expect(projectDir + .childDirectory('linux') + .childDirectory('test') + .childFile('flutter_project_plugin_test.cc'), exists); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true), + Logger: () => logger, + }); + testUsingContext('create a module with --platforms throws error.', () async { Cache.flutterRoot = '../..';