diff --git a/.ci.yaml b/.ci.yaml index 480dd8116cee6..1aa5993ddd9a9 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 32627ce013efd..dac94ada50c73 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 0000000000000..8e2a9e2909c7e --- /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 16d9d5598661f..c0e2854a3b993 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 f2269110c9998..a4fc907bec376 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 40a885d92f208..2d8110fe75c7b 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 4fa617a9ca4f9..4eeb03c7578b8 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 0000000000000..604a14e30ec3c --- /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 0000000000000..c5bfdde8201fc --- /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 2ba43bc268c23..d0902d5edc286 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 0f285de84f2a9..b1c802a890fa0 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 = '../..';