From 89906dedec493ccdf46dd557882ab401c712d4aa Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Thu, 11 Aug 2022 12:01:01 -0700 Subject: [PATCH] [Windows] Support native functions in test fixtures Adds the ability to register native functions for use in test fixtures. This allows registering native C++ functions that can be invoked from Dart code to perform the following common actions: * Signal a waiting latch in the C++ part of the test. * Pass data back to the C++ part of the test. * Allow the C++ part of the test to pass data to the test. Fixes: https://github.com/flutter/flutter/issues/109242 Fixes: https://github.com/flutter/flutter/issues/87299 --- shell/platform/windows/BUILD.gn | 2 + shell/platform/windows/fixtures/main.dart | 27 ++++- .../windows/flutter_windows_engine.cc | 6 ++ .../platform/windows/flutter_windows_engine.h | 17 +++ .../windows/flutter_windows_unittests.cc | 101 ++++++++++++++---- .../testing/windows_test_config_builder.cc | 7 ++ .../windows/testing/windows_test_context.cc | 24 ++++- .../windows/testing/windows_test_context.h | 17 +++ 8 files changed, 177 insertions(+), 24 deletions(-) diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index d2df59b2cf740..194e17f1f1e6d 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -225,6 +225,8 @@ executable("flutter_windows_unittests") { "//flutter/shell/platform/embedder:embedder_as_internal_library", "//flutter/shell/platform/embedder:embedder_test_utils", "//flutter/testing", + "//flutter/testing:dart", + "//flutter/third_party/tonic", "//third_party/rapidjson", ] } diff --git a/shell/platform/windows/fixtures/main.dart b/shell/platform/windows/fixtures/main.dart index 219c83e8eb6d5..2799c2f47ecf5 100644 --- a/shell/platform/windows/fixtures/main.dart +++ b/shell/platform/windows/fixtures/main.dart @@ -2,11 +2,34 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// Signals a waiting latch in the native test. +void signal() native 'Signal'; + +// Signals a waiting latch in the native test, passing a boolean value. +void signalBoolValue(bool value) native 'SignalBoolValue'; + +// Signals a waiting latch in the native test, which returns a value to the fixture. +bool signalBoolReturn() native 'SignalBoolReturn'; + void main() { - print('Hello windows engine test main!'); } @pragma('vm:entry-point') void customEntrypoint() { - print('Hello windows engine test customEntrypoint!'); +} + +@pragma('vm:entry-point') +void verifyNativeFunction() { + signal(); +} + +@pragma('vm:entry-point') +void verifyNativeFunctionWithParameters() { + signalBoolValue(true); +} + +@pragma('vm:entry-point') +void verifyNativeFunctionWithReturn() { + bool value = signalBoolReturn(); + signalBoolValue(value); } diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc index a0358bb6090d8..54d68f232885b 100644 --- a/shell/platform/windows/flutter_windows_engine.cc +++ b/shell/platform/windows/flutter_windows_engine.cc @@ -319,6 +319,12 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) { host->accessibility_bridge_->AddFlutterSemanticsCustomActionUpdate( action); }; + args.root_isolate_create_callback = [](void* user_data) { + auto host = static_cast(user_data); + if (host->root_isolate_create_callback_) { + host->root_isolate_create_callback_(); + } + }; args.custom_task_runners = &custom_task_runners; diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h index 1512870a4e710..c3c0d6109d922 100644 --- a/shell/platform/windows/flutter_windows_engine.h +++ b/shell/platform/windows/flutter_windows_engine.h @@ -13,6 +13,7 @@ #include #include +#include "flutter/fml/closure.h" #include "flutter/shell/platform/common/accessibility_bridge.h" #include "flutter/shell/platform/common/client_wrapper/binary_messenger_impl.h" #include "flutter/shell/platform/common/client_wrapper/include/flutter/basic_message_channel.h" @@ -203,6 +204,19 @@ class FlutterWindowsEngine { // Returns the native accessibility node with the given id. gfx::NativeViewAccessible GetNativeAccessibleFromId(AccessibilityNodeId id); + // Register a root isolate create callback. + // + // The root isolate create callback is invoked at creation of the root Dart + // isolate in the app. This may be used to be notified that execution of the + // main Dart entrypoint is about to begin, and is used by test infrastructure + // to register a native function resolver that can register and resolve + // functions marked as native in the Dart code. + // + // This must be called before calling |Run|. + void SetRootIsolateCreateCallback(const fml::closure& callback) { + root_isolate_create_callback_ = callback; + } + private: // Allows swapping out embedder_api_ calls in tests. friend class EngineModifier; @@ -277,6 +291,9 @@ class FlutterWindowsEngine { // The manager for WindowProc delegate registration and callbacks. std::unique_ptr window_proc_delegate_manager_; + + // The root isolate creation callback. + fml::closure root_isolate_create_callback_; }; } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_unittests.cc b/shell/platform/windows/flutter_windows_unittests.cc index ff3d00d0e6fb1..4479e4cd02617 100644 --- a/shell/platform/windows/flutter_windows_unittests.cc +++ b/shell/platform/windows/flutter_windows_unittests.cc @@ -6,14 +6,19 @@ #include +#include "flutter/fml/synchronization/count_down_latch.h" +#include "flutter/fml/synchronization/waitable_event.h" #include "flutter/shell/platform/windows/testing/windows_test.h" #include "flutter/shell/platform/windows/testing/windows_test_config_builder.h" #include "flutter/shell/platform/windows/testing/windows_test_context.h" #include "gtest/gtest.h" +#include "third_party/tonic/converter/dart_converter.h" namespace flutter { namespace testing { +// Verify that we can fetch a texture registrar. +// Prevent regression: https://github.com/flutter/flutter/issues/86617 TEST(WindowsNoFixtureTest, GetTextureRegistrar) { FlutterDesktopEngineProperties properties = {}; properties.assets_path = L""; @@ -25,33 +30,21 @@ TEST(WindowsNoFixtureTest, GetTextureRegistrar) { FlutterDesktopEngineDestroy(engine); } +// Verify we can successfully launch main(). TEST_F(WindowsTest, LaunchMain) { auto& context = GetContext(); WindowsConfigBuilder builder(context); ViewControllerPtr controller{builder.Run()}; ASSERT_NE(controller, nullptr); - - // Run for 1 second, then shut down. - // - // TODO(cbracken): Support registring a native function we can use to - // determine that execution has made it to a specific point in the Dart - // code. https://github.com/flutter/flutter/issues/109242 - std::this_thread::sleep_for(std::chrono::seconds(1)); } +// Verify we can successfully launch a custom entry point. TEST_F(WindowsTest, LaunchCustomEntrypoint) { auto& context = GetContext(); WindowsConfigBuilder builder(context); builder.SetDartEntrypoint("customEntrypoint"); ViewControllerPtr controller{builder.Run()}; ASSERT_NE(controller, nullptr); - - // Run for 1 second, then shut down. - // - // TODO(cbracken): Support registring a native function we can use to - // determine that execution has made it to a specific point in the Dart - // code. https://github.com/flutter/flutter/issues/109242 - std::this_thread::sleep_for(std::chrono::seconds(1)); } // Verify that engine launches with the custom entrypoint specified in the @@ -66,13 +59,6 @@ TEST_F(WindowsTest, LaunchCustomEntrypointInEngineRunInvocation) { ASSERT_NE(engine, nullptr); ASSERT_TRUE(FlutterDesktopEngineRun(engine.get(), "customEntrypoint")); - - // Run for 1 second, then shut down. - // - // TODO(cbracken): Support registring a native function we can use to - // determine that execution has made it to a specific point in the Dart - // code. https://github.com/flutter/flutter/issues/109242 - std::this_thread::sleep_for(std::chrono::seconds(1)); } // Verify that engine fails to launch when a conflicting entrypoint in @@ -90,5 +76,78 @@ TEST_F(WindowsTest, LaunchConflictingCustomEntrypoints) { ASSERT_FALSE(FlutterDesktopEngineRun(engine.get(), "conflictingEntrypoint")); } +// Verify that native functions can be registered and resolved. +TEST_F(WindowsTest, VerifyNativeFunction) { + auto& context = GetContext(); + WindowsConfigBuilder builder(context); + builder.SetDartEntrypoint("verifyNativeFunction"); + + fml::AutoResetWaitableEvent latch; + auto native_entry = + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); }); + context.AddNativeFunction("Signal", native_entry); + + ViewControllerPtr controller{builder.Run()}; + ASSERT_NE(controller, nullptr); + + // Wait until signal has been called. + latch.Wait(); +} + +// Verify that native functions that pass parameters can be registered and +// resolved. +TEST_F(WindowsTest, VerifyNativeFunctionWithParameters) { + auto& context = GetContext(); + WindowsConfigBuilder builder(context); + builder.SetDartEntrypoint("verifyNativeFunctionWithParameters"); + + bool bool_value = false; + fml::AutoResetWaitableEvent latch; + auto native_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + auto handle = Dart_GetNativeBooleanArgument(args, 0, &bool_value); + ASSERT_FALSE(Dart_IsError(handle)); + latch.Signal(); + }); + context.AddNativeFunction("SignalBoolValue", native_entry); + + ViewControllerPtr controller{builder.Run()}; + ASSERT_NE(controller, nullptr); + + // Wait until signalBoolValue has been called. + latch.Wait(); + EXPECT_TRUE(bool_value); +} + +// Verify that native functions that return values can be registered and +// resolved. +TEST_F(WindowsTest, VerifyNativeFunctionWithReturn) { + auto& context = GetContext(); + WindowsConfigBuilder builder(context); + builder.SetDartEntrypoint("verifyNativeFunctionWithReturn"); + + bool bool_value_to_return = true; + fml::CountDownLatch latch(2); + auto bool_return_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + Dart_SetBooleanReturnValue(args, bool_value_to_return); + latch.CountDown(); + }); + context.AddNativeFunction("SignalBoolReturn", bool_return_entry); + + bool bool_value_passed = false; + auto bool_pass_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + auto handle = Dart_GetNativeBooleanArgument(args, 0, &bool_value_passed); + ASSERT_FALSE(Dart_IsError(handle)); + latch.CountDown(); + }); + context.AddNativeFunction("SignalBoolValue", bool_pass_entry); + + ViewControllerPtr controller{builder.Run()}; + ASSERT_NE(controller, nullptr); + + // Wait until signalBoolReturn and signalBoolValue have been called. + latch.Wait(); + EXPECT_TRUE(bool_value_passed); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/testing/windows_test_config_builder.cc b/shell/platform/windows/testing/windows_test_config_builder.cc index c3c2d0ecf9680..35376e241f062 100644 --- a/shell/platform/windows/testing/windows_test_config_builder.cc +++ b/shell/platform/windows/testing/windows_test_config_builder.cc @@ -11,6 +11,7 @@ #include #include "flutter/fml/logging.h" +#include "flutter/shell/platform/windows/flutter_windows_engine.h" #include "flutter/shell/platform/windows/public/flutter_windows.h" #include "flutter/shell/platform/windows/testing/windows_test_context.h" @@ -78,6 +79,12 @@ ViewControllerPtr WindowsConfigBuilder::Run() const { return {}; } + // Register native functions. + FlutterWindowsEngine* windows_engine = + reinterpret_cast(engine.get()); + windows_engine->SetRootIsolateCreateCallback( + context_.GetRootIsolateCallback()); + int width = 600; int height = 400; ViewControllerPtr controller( diff --git a/shell/platform/windows/testing/windows_test_context.cc b/shell/platform/windows/testing/windows_test_context.cc index 42da7c1651341..87ba928fbbe74 100644 --- a/shell/platform/windows/testing/windows_test_context.cc +++ b/shell/platform/windows/testing/windows_test_context.cc @@ -10,7 +10,16 @@ namespace flutter { namespace testing { WindowsTestContext::WindowsTestContext(std::string_view assets_path) - : assets_path_(fml::Utf8ToWideString(assets_path)) {} + : assets_path_(fml::Utf8ToWideString(assets_path)), + native_resolver_(std::make_shared()) { + isolate_create_callbacks_.push_back( + [weak_resolver = + std::weak_ptr{native_resolver_}]() { + if (auto resolver = weak_resolver.lock()) { + resolver->SetNativeResolverForIsolate(); + } + }); +} WindowsTestContext::~WindowsTestContext() = default; @@ -22,5 +31,18 @@ const std::wstring& WindowsTestContext::GetIcuDataPath() const { return icu_data_path_; } +void WindowsTestContext::AddNativeFunction(std::string_view name, + Dart_NativeFunction function) { + native_resolver_->AddNativeCallback(std::string{name}, function); +} + +fml::closure WindowsTestContext::GetRootIsolateCallback() { + return [this]() { + for (auto closure : this->isolate_create_callbacks_) { + closure(); + } + }; +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/testing/windows_test_context.h b/shell/platform/windows/testing/windows_test_context.h index bdc6fcc22a2de..a1025a8c3b24f 100644 --- a/shell/platform/windows/testing/windows_test_context.h +++ b/shell/platform/windows/testing/windows_test_context.h @@ -7,8 +7,11 @@ #include #include +#include +#include "flutter/fml/closure.h" #include "flutter/fml/macros.h" +#include "flutter/testing/test_dart_native_resolver.h" namespace flutter { namespace testing { @@ -27,9 +30,23 @@ class WindowsTestContext { const std::wstring& GetIcuDataPath() const; + // Registers a native function callable from Dart code in test fixtures. In + // the Dart test fixture, the associated function can be declared as: + // + // ReturnType functionName() native 'IdentifyingName'; + // + // where `IdentifyingName` matches the |name| parameter to this method. + void AddNativeFunction(std::string_view name, Dart_NativeFunction function); + + // Returns the root isolate create callback to register with the Flutter + // runtime. + fml::closure GetRootIsolateCallback(); + private: std::wstring assets_path_; std::wstring icu_data_path_ = L"icudtl.dat"; + std::vector isolate_create_callbacks_; + std::shared_ptr native_resolver_; FML_DISALLOW_COPY_AND_ASSIGN(WindowsTestContext); };