diff --git a/BUILD.gn b/BUILD.gn index 86c63580228e3..49784eff36e03 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -168,6 +168,7 @@ group("unittests") { public_deps = [] if (is_android) { public_deps += [ + "//flutter/impeller/toolkit/android:apk_unittests", "//flutter/impeller/toolkit/android:unittests", "//flutter/shell/platform/android:flutter_shell_native_unittests", ] diff --git a/impeller/toolkit/android/BUILD.gn b/impeller/toolkit/android/BUILD.gn index fd0dde36bd9bf..e0f83d652b573 100644 --- a/impeller/toolkit/android/BUILD.gn +++ b/impeller/toolkit/android/BUILD.gn @@ -2,6 +2,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import("//flutter/testing/android/native_activity/native_activity.gni") import("../../tools/impeller.gni") config("public_android_config") { @@ -37,13 +38,11 @@ test_fixtures("unittests_fixtures") { fixtures = [] } -executable("unittests") { - assert(is_android) +source_set("unittests_lib") { + visibility = [ ":*" ] testonly = true - output_name = "impeller_toolkit_android_unittests" - sources = [ "toolkit_android_unittests.cc" ] deps = [ @@ -52,3 +51,24 @@ executable("unittests") { "//flutter/testing", ] } + +executable("unittests") { + assert(is_android) + + testonly = true + + output_name = "impeller_toolkit_android_unittests" + + deps = [ ":unittests_lib" ] +} + +native_activity_apk("apk_unittests") { + apk_name = "impeller_toolkit_android_unittests" + + testonly = true + + deps = [ + ":unittests_lib", + "//flutter/testing/android/native_activity:gtest_activity", + ] +} diff --git a/testing/BUILD.gn b/testing/BUILD.gn index 95b58199a714a..28dab99a0b301 100644 --- a/testing/BUILD.gn +++ b/testing/BUILD.gn @@ -21,6 +21,8 @@ source_set("testing_lib") { "debugger_detection.h", "display_list_testing.cc", "display_list_testing.h", + "logger_listener.cc", + "logger_listener.h", "mock_canvas.cc", "mock_canvas.h", "post_task_sync.cc", diff --git a/testing/android/native_activity/AndroidManifest.xml.template b/testing/android/native_activity/AndroidManifest.xml.template new file mode 100644 index 0000000000000..9f908332793a7 --- /dev/null +++ b/testing/android/native_activity/AndroidManifest.xml.template @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/testing/android/native_activity/BUILD.gn b/testing/android/native_activity/BUILD.gn new file mode 100644 index 0000000000000..354864c1c97a3 --- /dev/null +++ b/testing/android/native_activity/BUILD.gn @@ -0,0 +1,41 @@ +# Copyright 2013 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. + +# To create an native activity, deps in this source set in a +# `native_activity_apk` target and make sure to add the implementation of +# `NativeActivityMain` which returns a `flutter::NativeActivity` subclass. +source_set("native_activity") { + assert(is_android) + + sources = [ + "native_activity.cc", + "native_activity.h", + ] + + public_deps = [ + "//flutter/fml", + "//flutter/impeller/toolkit/android", + ] + + libs = [ + "android", + "log", + ] +} + +source_set("gtest_activity") { + assert(is_android) + + testonly = true + + sources = [ + "gtest_activity.cc", + "gtest_activity.h", + ] + + public_deps = [ + ":native_activity", + "//flutter/testing:testing_lib", + ] +} diff --git a/testing/android/native_activity/README.md b/testing/android/native_activity/README.md new file mode 100644 index 0000000000000..5ce198a06691d --- /dev/null +++ b/testing/android/native_activity/README.md @@ -0,0 +1,31 @@ +Native Activity +=============== + +Executables packaged as native activities in an Android APK. These activities +contain no Java code. + +To create an APK of your existing `exectuable` target, replace `exectuable` with +`native_activity_apk` from the `native_activity.gni` template and give it an +`apk_name`. + +## Example + +``` +native_activity_apk("apk_unittests") { + apk_name = "toolkit_unittests" + + testonly = true + + sources = [ "toolkit_android_unittests.cc" ] + + deps = [ + ":unittests_lib", + "//flutter/testing/android/native_activity:gtest_activity", + ] +} +``` + +One of the translation units in must contain an implementation of +`flutter::NativeActivityMain`. The `gtest_activity` target contains an +implementation of an activity that run GoogleTests. That can be used off the +shelf. diff --git a/testing/android/native_activity/debug.keystore b/testing/android/native_activity/debug.keystore new file mode 100644 index 0000000000000..df0714f9b96d2 Binary files /dev/null and b/testing/android/native_activity/debug.keystore differ diff --git a/testing/android/native_activity/gtest_activity.cc b/testing/android/native_activity/gtest_activity.cc new file mode 100644 index 0000000000000..4a678f36e6b98 --- /dev/null +++ b/testing/android/native_activity/gtest_activity.cc @@ -0,0 +1,49 @@ +// Copyright 2013 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. + +#include "flutter/testing/android/native_activity/gtest_activity.h" + +#include "flutter/impeller/toolkit/android/native_window.h" +#include "flutter/testing/logger_listener.h" +#include "flutter/testing/test_timeout_listener.h" + +namespace flutter { + +GTestActivity::GTestActivity(ANativeActivity* activity) + : NativeActivity(activity) {} + +GTestActivity::~GTestActivity() = default; + +static void StartTestSuite(const impeller::android::NativeWindow& window) { + auto timeout_listener = new flutter::testing::TestTimeoutListener( + fml::TimeDelta::FromSeconds(120u)); + auto logger_listener = new flutter::testing::LoggerListener(); + + auto& listeners = ::testing::UnitTest::GetInstance()->listeners(); + + listeners.Append(timeout_listener); + listeners.Append(logger_listener); + + int result = RUN_ALL_TESTS(); + + delete listeners.Release(timeout_listener); + delete listeners.Release(logger_listener); + + FML_CHECK(result == 0); +} + +// |NativeActivity| +void GTestActivity::OnNativeWindowCreated(ANativeWindow* window) { + auto handle = std::make_shared(window); + background_thread_.GetTaskRunner()->PostTask( + [handle]() { StartTestSuite(*handle); }); +} + +std::unique_ptr NativeActivityMain( + ANativeActivity* activity, + std::unique_ptr saved_state) { + return std::make_unique(activity); +} + +} // namespace flutter diff --git a/testing/android/native_activity/gtest_activity.h b/testing/android/native_activity/gtest_activity.h new file mode 100644 index 0000000000000..44a0a5eb74557 --- /dev/null +++ b/testing/android/native_activity/gtest_activity.h @@ -0,0 +1,40 @@ +// Copyright 2013 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. + +#ifndef FLUTTER_TESTING_ANDROID_NATIVE_ACTIVITY_GTEST_ACTIVITY_H_ +#define FLUTTER_TESTING_ANDROID_NATIVE_ACTIVITY_GTEST_ACTIVITY_H_ + +#include "flutter/fml/macros.h" +#include "flutter/fml/thread.h" +#include "flutter/testing/android/native_activity/native_activity.h" + +namespace flutter { + +//------------------------------------------------------------------------------ +/// @brief A native activity subclass an in implementation of +/// `flutter::NativeActivityMain` that return it. +/// +/// This class runs a Google Test harness on a background thread and +/// redirects progress updates to `logcat` instead of STDOUT. +/// +class GTestActivity final : public NativeActivity { + public: + explicit GTestActivity(ANativeActivity* activity); + + ~GTestActivity() override; + + GTestActivity(const GTestActivity&) = delete; + + GTestActivity& operator=(const GTestActivity&) = delete; + + // |NativeActivity| + void OnNativeWindowCreated(ANativeWindow* window) override; + + private: + fml::Thread background_thread_; +}; + +} // namespace flutter + +#endif // FLUTTER_TESTING_ANDROID_NATIVE_ACTIVITY_GTEST_ACTIVITY_H_ diff --git a/testing/android/native_activity/native_activity.cc b/testing/android/native_activity/native_activity.cc new file mode 100644 index 0000000000000..37f7a1d7652e8 --- /dev/null +++ b/testing/android/native_activity/native_activity.cc @@ -0,0 +1,143 @@ +// Copyright 2013 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. + +#include "flutter/testing/android/native_activity/native_activity.h" + +#include "flutter/fml/message_loop.h" + +namespace flutter { + +NativeActivity::NativeActivity(ANativeActivity* activity) + : activity_(activity) { + fml::MessageLoop::EnsureInitializedForCurrentThread(); + + activity->instance = this; + + activity->callbacks->onStart = [](ANativeActivity* activity) { + reinterpret_cast(activity->instance)->OnStart(); + }; + activity->callbacks->onStop = [](ANativeActivity* activity) { + reinterpret_cast(activity->instance)->OnStop(); + }; + activity->callbacks->onPause = [](ANativeActivity* activity) { + reinterpret_cast(activity->instance)->OnPause(); + }; + activity->callbacks->onResume = [](ANativeActivity* activity) { + reinterpret_cast(activity->instance)->OnResume(); + }; + activity->callbacks->onDestroy = [](ANativeActivity* activity) { + delete reinterpret_cast(activity->instance); + }; + activity->callbacks->onSaveInstanceState = [](ANativeActivity* activity, + size_t* out_size) -> void* { + auto mapping = reinterpret_cast(activity->instance) + ->OnSaveInstanceState(); + if (mapping == nullptr || mapping->GetMapping() == nullptr) { + *out_size = 0; + return nullptr; + } + + // This will be `free`d by the framework. + auto copied = malloc(mapping->GetSize()); + FML_CHECK(copied != nullptr) + << "Allocation failure while saving instance state."; + memcpy(copied, mapping->GetMapping(), mapping->GetSize()); + *out_size = mapping->GetSize(); + return copied; + }; + activity->callbacks->onWindowFocusChanged = [](ANativeActivity* activity, + int has_focus) { + reinterpret_cast(activity->instance) + ->OnWindowFocusChanged(has_focus); + }; + activity->callbacks->onNativeWindowCreated = [](ANativeActivity* activity, + ANativeWindow* window) { + reinterpret_cast(activity->instance) + ->OnNativeWindowCreated(window); + }; + activity->callbacks->onNativeWindowResized = [](ANativeActivity* activity, + ANativeWindow* window) { + reinterpret_cast(activity->instance) + ->OnNativeWindowResized(window); + }; + activity->callbacks->onNativeWindowRedrawNeeded = + [](ANativeActivity* activity, ANativeWindow* window) { + reinterpret_cast(activity->instance) + ->OnNativeWindowRedrawNeeded(window); + }; + activity->callbacks->onNativeWindowDestroyed = [](ANativeActivity* activity, + ANativeWindow* window) { + reinterpret_cast(activity->instance) + ->OnNativeWindowDestroyed(window); + }; + activity->callbacks->onInputQueueCreated = [](ANativeActivity* activity, + AInputQueue* queue) { + reinterpret_cast(activity->instance) + ->OnInputQueueCreated(queue); + }; + activity->callbacks->onInputQueueDestroyed = [](ANativeActivity* activity, + AInputQueue* queue) { + reinterpret_cast(activity->instance) + ->OnInputQueueDestroyed(queue); + }; + activity->callbacks->onConfigurationChanged = [](ANativeActivity* activity) { + reinterpret_cast(activity->instance) + ->OnConfigurationChanged(); + }; + activity->callbacks->onLowMemory = [](ANativeActivity* activity) { + reinterpret_cast(activity->instance)->OnLowMemory(); + }; +} + +NativeActivity::~NativeActivity() = default; + +void NativeActivity::OnStart() {} + +void NativeActivity::OnStop() {} + +void NativeActivity::OnPause() {} + +void NativeActivity::OnResume() {} + +std::shared_ptr NativeActivity::OnSaveInstanceState() { + return nullptr; +} + +void NativeActivity::OnWindowFocusChanged(bool has_focus) {} + +void NativeActivity::OnNativeWindowCreated(ANativeWindow* window) {} + +void NativeActivity::OnNativeWindowResized(ANativeWindow* window) {} + +void NativeActivity::OnNativeWindowRedrawNeeded(ANativeWindow* window) {} + +void NativeActivity::OnNativeWindowDestroyed(ANativeWindow* window) {} + +void NativeActivity::OnInputQueueCreated(AInputQueue* queue) {} + +void NativeActivity::OnInputQueueDestroyed(AInputQueue* queue) {} + +void NativeActivity::OnConfigurationChanged() {} + +void NativeActivity::OnLowMemory() {} + +void NativeActivity::Terminate() { + ANativeActivity_finish(activity_); +} + +} // namespace flutter + +extern "C" __attribute__((visibility("default"))) void ANativeActivity_onCreate( + ANativeActivity* activity, + void* saved_state, + size_t saved_state_size) { + std::unique_ptr saved_state_mapping; + if (saved_state_size > 0u) { + saved_state_mapping = std::make_unique( + fml::MallocMapping::Copy(saved_state, saved_state_size)); + } + flutter::NativeActivityMain(activity, std::move(saved_state_mapping)) + .release(); // Will be freed when the frame calls the onDestroy. See the + // delete in that callback. +} diff --git a/testing/android/native_activity/native_activity.gni b/testing/android/native_activity/native_activity.gni new file mode 100644 index 0000000000000..cbf4d3e75141d --- /dev/null +++ b/testing/android/native_activity/native_activity.gni @@ -0,0 +1,92 @@ +# Copyright 2013 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("//build/config/android/config.gni") +import("//flutter/tools/templater/templater.gni") + +android_buildtools = "//third_party/android_tools/sdk/build-tools/34.0.0" +aapt2 = "$android_buildtools/aapt2" +zipalign = "$android_buildtools/zipalign" +apksigner = "$android_buildtools/apksigner" +android_jar = "//third_party/android_tools/sdk/platforms/android-34/android.jar" +src_root = "//flutter/testing/android/native_activity" + +# A drop in replacement for an executable or shared library target. Providing a +# (required) apk_name packages that native code into an APK suitable for +# debugging. +template("native_activity_apk") { + assert(defined(invoker.apk_name), "The name of the APK must be specified.") + + invoker_apk_name = invoker.apk_name + apk_dylib_name = "lib$invoker_apk_name.so" + + android_manifest_template = "$src_root/AndroidManifest.xml.template" + android_manifest = "$target_gen_dir/AndroidManifest.xml" + + android_manifest_target_name = "android_manifest_$target_name" + templater(android_manifest_target_name) { + input = android_manifest_template + output = android_manifest + values = [ "--apk-library-name=$invoker_apk_name" ] + } + + shared_library_target_name = "shared_library_$target_name" + shared_library(shared_library_target_name) { + forward_variables_from(invoker, "*", [ "output_name" ]) + output_name = invoker_apk_name + } + + apk_target_name = "apk_$target_name" + action(apk_target_name) { + forward_variables_from(invoker, [ "testonly" ]) + + script = "$src_root/native_activity_apk.py" + + apk_path = "$root_build_dir/$invoker_apk_name.apk" + + sources = [ + "$root_build_dir/$apk_dylib_name", + aapt2, + android_jar, + android_manifest_template, + apksigner, + zipalign, + ] + + outputs = [ apk_path ] + + args = [ + "--aapt2-bin", + rebase_path(aapt2, root_build_dir), + "--zipalign-bin", + rebase_path(zipalign, root_build_dir), + "--android-manifest", + rebase_path(android_manifest, root_build_dir), + "--android-jar", + rebase_path(android_jar, root_build_dir), + "--output-path", + rebase_path(apk_path, root_build_dir), + "--library", + rebase_path("$root_build_dir/$apk_dylib_name", root_build_dir), + "--apksigner-bin", + rebase_path(apksigner, root_build_dir), + "--keystore", + rebase_path("$src_root/debug.keystore", root_build_dir), + "--gen-dir", + rebase_path(target_gen_dir, root_build_dir), + "--android-abi", + android_app_abi, + ] + deps = [ + ":$android_manifest_target_name", + ":$shared_library_target_name", + ] + } + + group(target_name) { + forward_variables_from(invoker, [ "testonly" ]) + + deps = [ ":$apk_target_name" ] + } +} diff --git a/testing/android/native_activity/native_activity.h b/testing/android/native_activity/native_activity.h new file mode 100644 index 0000000000000..d740d87486942 --- /dev/null +++ b/testing/android/native_activity/native_activity.h @@ -0,0 +1,81 @@ +// Copyright 2013 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. + +#ifndef FLUTTER_TESTING_ANDROID_NATIVE_ACTIVITY_NATIVE_ACTIVITY_H_ +#define FLUTTER_TESTING_ANDROID_NATIVE_ACTIVITY_NATIVE_ACTIVITY_H_ + +#include + +#include "flutter/fml/macros.h" +#include "flutter/fml/mapping.h" + +namespace flutter { + +//------------------------------------------------------------------------------ +/// @brief An instance of a native activity. Users of the +/// `native_activity_apk` are meant to subclass this and return an +/// instance of this subclass from `flutter::NativeActivityMain`. +/// +/// All methods are called on the Android Platform main-thread. +/// Subclasses will usually re-thread calls to a background thread +/// for long running tasks as these will lead to ANRs on when +/// invoked on the platform thread. +/// +class NativeActivity { + public: + virtual ~NativeActivity(); + + //---------------------------------------------------------------------------- + /// @brief Perform graceful termination of the activity. Will eventually + /// lead to the other activity lifecycle callback on the way to + /// termination. + /// + /// Can be called from any thread. + /// + void Terminate(); + + virtual void OnStart(); + + virtual void OnStop(); + + virtual void OnPause(); + + virtual void OnResume(); + + virtual std::shared_ptr OnSaveInstanceState(); + + virtual void OnWindowFocusChanged(bool has_focus); + + virtual void OnNativeWindowCreated(ANativeWindow* window); + + virtual void OnNativeWindowResized(ANativeWindow* window); + + virtual void OnNativeWindowRedrawNeeded(ANativeWindow* window); + + virtual void OnNativeWindowDestroyed(ANativeWindow* window); + + virtual void OnInputQueueCreated(AInputQueue* queue); + + virtual void OnInputQueueDestroyed(AInputQueue* queue); + + virtual void OnConfigurationChanged(); + + virtual void OnLowMemory(); + + protected: + explicit NativeActivity(ANativeActivity* activity); + + private: + ANativeActivity* activity_ = nullptr; + + FML_DISALLOW_COPY_AND_ASSIGN(NativeActivity); +}; + +std::unique_ptr NativeActivityMain( + ANativeActivity* activity, + std::unique_ptr saved_state); + +} // namespace flutter + +#endif // FLUTTER_TESTING_ANDROID_NATIVE_ACTIVITY_NATIVE_ACTIVITY_H_ diff --git a/testing/android/native_activity/native_activity_apk.py b/testing/android/native_activity/native_activity_apk.py new file mode 100644 index 0000000000000..23813ab969048 --- /dev/null +++ b/testing/android/native_activity/native_activity_apk.py @@ -0,0 +1,97 @@ +# Copyright 2013 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 sys + +import argparse +import os +import zipfile +import subprocess + + +def run_command_checked(command): + try: + subprocess.check_output(command, stderr=subprocess.STDOUT, text=True) + except subprocess.CalledProcessError as cpe: + print(cpe.output) + raise cpe + + +def main(): + parser = argparse.ArgumentParser() + + parser.add_argument('--aapt2-bin', type=str, required=True, help='The path to the aapt2 binary.') + parser.add_argument( + '--zipalign-bin', type=str, required=True, help='The path to the zipalign binary.' + ) + parser.add_argument( + '--apksigner-bin', type=str, required=True, help='The path to the apksigner binary.' + ) + parser.add_argument( + '--android-manifest', type=str, required=True, help='The path to the AndroidManifest.xml.' + ) + parser.add_argument('--android-jar', type=str, required=True, help='The path to android.jar.') + parser.add_argument('--output-path', type=str, required=True, help='The path to the output apk.') + parser.add_argument( + '--library', type=str, required=True, help='The path to the library to put in the apk.' + ) + parser.add_argument( + '--keystore', type=str, required=True, help='The path to the debug keystore to sign the apk.' + ) + parser.add_argument( + '--gen-dir', type=str, required=True, help='The directory for generated files.' + ) + parser.add_argument( + '--android-abi', type=str, required=True, help='The android ABI of the library.' + ) + + args = parser.parse_args() + + library_file = os.path.basename(args.library) + apk_name = os.path.basename(args.output_path) + + unaligned_apk_path = os.path.join(args.gen_dir, '%s.unaligned' % apk_name) + unsigned_apk_path = os.path.join(args.gen_dir, '%s.unsigned' % apk_name) + apk_path = args.output_path + + # Create the skeleton of the APK using aapt2. + aapt2_command = [ + args.aapt2_bin, + 'link', + '-I', + args.android_jar, + '--manifest', + args.android_manifest, + '-o', + unaligned_apk_path, + ] + run_command_checked(aapt2_command) + + # Stuff the library in the APK which is just a regular ZIP file. Libraries are not compressed. + with zipfile.ZipFile(unaligned_apk_path, 'a', compression=zipfile.ZIP_STORED) as zipf: + zipf.write(args.library, 'lib/%s/%s' % (args.android_abi, library_file)) + + # Align the dylib to a page boundary. + zipalign_command = [ + args.zipalign_bin, + '-p', # Page align the dylib + '-f', # overwrite output if exists + '4', # 32-bit alignment + unaligned_apk_path, + unsigned_apk_path, + ] + run_command_checked(zipalign_command) + + # Sign the APK. + apksigner_command = [ + args.apksigner_bin, 'sign', '--ks', args.keystore, '--ks-pass', 'pass:android', '--out', + apk_path, unsigned_apk_path + ] + run_command_checked(apksigner_command) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/testing/logger_listener.cc b/testing/logger_listener.cc new file mode 100644 index 0000000000000..a4ce8408636d4 --- /dev/null +++ b/testing/logger_listener.cc @@ -0,0 +1,58 @@ +// Copyright 2013 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. + +#include "flutter/testing/logger_listener.h" + +namespace flutter::testing { + +LoggerListener::LoggerListener() = default; + +LoggerListener::~LoggerListener() = default; + +void testing::LoggerListener::OnTestStart( + const ::testing::TestInfo& test_info) { + FML_LOG(IMPORTANT) << ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"; + FML_LOG(IMPORTANT) << "Starting Test: " << test_info.test_suite_name() << ":" + << test_info.name(); +} + +std::string TestStatusAsString(const ::testing::TestResult* result) { + if (result == nullptr) { + return "UNKNOWN"; + } + if (result->Passed()) { + return "PASSED"; + } + if (result->Skipped()) { + return "SKIPPED"; + } + if (result->Failed()) { + return "FAILED"; + } + return "UNKNOWN"; +} + +std::string TestLabel(const ::testing::TestInfo& info) { + return std::string{info.test_suite_name()} + "." + info.name(); +} + +std::string TestTimeAsString(const ::testing::TestResult* result) { + if (result == nullptr) { + return "UNKNOWN"; + } + return std::to_string(result->elapsed_time()) + " ms"; +} + +void testing::LoggerListener::OnTestEnd(const ::testing::TestInfo& info) { + FML_LOG(IMPORTANT) << "Test " << TestStatusAsString(info.result()) << " (" + << TestTimeAsString(info.result()) + << "): " << TestLabel(info); + FML_LOG(IMPORTANT) << "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; +} + +void testing::LoggerListener::OnTestDisabled(const ::testing::TestInfo& info) { + FML_LOG(IMPORTANT) << "Test Disabled: " << TestLabel(info); +} + +} // namespace flutter::testing diff --git a/testing/logger_listener.h b/testing/logger_listener.h new file mode 100644 index 0000000000000..92a4fafd1f504 --- /dev/null +++ b/testing/logger_listener.h @@ -0,0 +1,35 @@ +// Copyright 2013 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. + +#ifndef FLUTTER_TESTING_LOGGER_LISTENER_H_ +#define FLUTTER_TESTING_LOGGER_LISTENER_H_ + +#include "flutter/fml/logging.h" +#include "flutter/testing/testing.h" + +namespace flutter::testing { + +class LoggerListener : public ::testing::EmptyTestEventListener { + public: + LoggerListener(); + + ~LoggerListener(); + + LoggerListener(const LoggerListener&) = delete; + + LoggerListener& operator=(const LoggerListener&) = delete; + + // |testing::EmptyTestEventListener| + void OnTestStart(const ::testing::TestInfo& test_info) override; + + // |testing::EmptyTestEventListener| + void OnTestEnd(const ::testing::TestInfo& test_info) override; + + // |testing::EmptyTestEventListener| + void OnTestDisabled(const ::testing::TestInfo& test_info) override; +}; + +} // namespace flutter::testing + +#endif // FLUTTER_TESTING_LOGGER_LISTENER_H_ diff --git a/tools/templater/BUILD.gn b/tools/templater/BUILD.gn new file mode 100644 index 0000000000000..78bab468318f2 --- /dev/null +++ b/tools/templater/BUILD.gn @@ -0,0 +1,11 @@ +# Copyright 2013 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. + +executable("templater") { + sources = [ "templater_main.cc" ] + deps = [ + "//flutter/fml", + "//third_party/inja", + ] +} diff --git a/tools/templater/templater.gni b/tools/templater/templater.gni new file mode 100644 index 0000000000000..fc4b47cb97df8 --- /dev/null +++ b/tools/templater/templater.gni @@ -0,0 +1,31 @@ +# Copyright 2013 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("//build/compiled_action.gni") + +# Inflate the input template file using Inja and the specified values. +template("templater") { + assert(defined(invoker.input), "The input template must be specified.") + assert(defined(invoker.output), "The output location must be defined.") + assert( + defined(invoker.values), + "The values referenced in the template must be specified. Use the --key=value format for each value.") + + compiled_action(target_name) { + tool = "//flutter/tools/templater" + + inputs = [ invoker.input ] + outputs = [ invoker.output ] + + templater_input_path = rebase_path(invoker.input, root_build_dir) + templater_input_flag = "--templater-input=$templater_input_path" + templater_output_path = rebase_path(invoker.output, root_build_dir) + templater_output_flag = "--templater-output=$templater_output_path" + + args = [ + templater_input_flag, + templater_output_flag, + ] + invoker.values + } +} diff --git a/tools/templater/templater_main.cc b/tools/templater/templater_main.cc new file mode 100644 index 0000000000000..4cd0f230d8857 --- /dev/null +++ b/tools/templater/templater_main.cc @@ -0,0 +1,73 @@ +// Copyright 2013 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. + +// FLUTTER_NOLINT: https://github.com/flutter/flutter/issues/105732 + +#include +#include +#include + +#include "flutter/fml/command_line.h" +#include "flutter/fml/file.h" +#include "flutter/fml/logging.h" +#include "flutter/fml/mapping.h" +#include "inja/inja.hpp" + +namespace flutter { + +bool TemplaterMain(const fml::CommandLine& command_line) { + std::string input_path; + std::string output_path; + + if (!command_line.GetOptionValue("templater-input", &input_path)) { + FML_LOG(ERROR) + << "Input template path not specified. Use --templater-input."; + return false; + } + if (!command_line.GetOptionValue("templater-output", &output_path)) { + FML_LOG(ERROR) + << "Input template path not specified. Use --templater-output."; + return false; + } + + auto input = fml::FileMapping::CreateReadOnly(input_path); + if (!input) { + FML_LOG(ERROR) << "Could not open input file: " << input_path; + return false; + } + + nlohmann::json arguments; + for (const auto& option : command_line.options()) { + arguments[option.name] = option.value; + } + inja::Environment env; + auto rendered_template = env.render( + std::string_view{reinterpret_cast(input->GetMapping()), + input->GetSize()}, + arguments); + auto output = fml::NonOwnedMapping{ + reinterpret_cast(rendered_template.data()), + rendered_template.size()}; + + auto current_dir = + fml::OpenDirectory(std::filesystem::current_path().u8string().c_str(), + false, fml::FilePermission::kReadWrite); + if (!current_dir.is_valid()) { + FML_LOG(ERROR) << "Could not open current directory."; + return false; + } + if (!fml::WriteAtomically(current_dir, output_path.c_str(), output)) { + FML_LOG(ERROR) << "Could not write output to path: " << output_path; + return false; + } + return true; +} + +} // namespace flutter + +int main(int argc, char const* argv[]) { + return flutter::TemplaterMain(fml::CommandLineFromArgcArgv(argc, argv)) + ? EXIT_SUCCESS + : EXIT_FAILURE; +}