From cb5828068fc726e8dda14c605e5d6ebfec293f2d Mon Sep 17 00:00:00 2001 From: Filip Filmar Date: Fri, 23 Oct 2020 18:52:02 -0700 Subject: [PATCH] [fuchsia] Adds a test for timezone change (flutter/engine#21934) --- engine/src/flutter/shell/common/BUILD.gn | 11 + .../shell/common/fixtures/shell_test.dart | 27 +- .../shell/common/shell_fuchsia_unittests.cc | 257 ++++++++++++++++++ .../testing/fuchsia/meta/fuchsia_test.cmx | 11 +- 4 files changed, 295 insertions(+), 11 deletions(-) create mode 100644 engine/src/flutter/shell/common/shell_fuchsia_unittests.cc diff --git a/engine/src/flutter/shell/common/BUILD.gn b/engine/src/flutter/shell/common/BUILD.gn index c988e88c2a1..2cc266712c2 100644 --- a/engine/src/flutter/shell/common/BUILD.gn +++ b/engine/src/flutter/shell/common/BUILD.gn @@ -255,5 +255,16 @@ if (enable_unittests) { "//flutter/shell/version", "//third_party/googletest:gmock", ] + + if (is_fuchsia) { + sources += [ "shell_fuchsia_unittests.cc" ] + + deps += [ + "$fuchsia_sdk_root/fidl:fuchsia.intl", + "$fuchsia_sdk_root/fidl:fuchsia.settings", + "$fuchsia_sdk_root/pkg:fidl_cpp", + "$fuchsia_sdk_root/pkg:sys_cpp", + ] + } } } diff --git a/engine/src/flutter/shell/common/fixtures/shell_test.dart b/engine/src/flutter/shell/common/fixtures/shell_test.dart index 469dc250bea..1e87d134c75 100644 --- a/engine/src/flutter/shell/common/fixtures/shell_test.dart +++ b/engine/src/flutter/shell/common/fixtures/shell_test.dart @@ -164,14 +164,29 @@ List getFixtureImage() native 'GetFixtureImage'; void notifyLocalTime(String string) native 'NotifyLocalTime'; +bool waitFixture() native 'WaitFixture'; + +// Return local date-time as a string, to an hour resolution. So, "2020-07-23 +// 14:03:22" will become "2020-07-23 14". +String localTimeAsString() { + final now = DateTime.now().toLocal(); + // This is: "$y-$m-$d $h:$min:$sec.$ms$us"; + final timeStr = now.toString(); + // Forward only "$y-$m-$d $h" for timestamp comparison. Not using DateTime + // formatting since package:intl is not available. + return timeStr.split(":")[0]; +} + @pragma('vm:entry-point') void localtimesMatch() { - final now = DateTime.now().toLocal(); - // This is: "$y-$m-$d $h:$min:$sec.$ms$us"; - final timeStr = now.toString(); - // Forward only "$y-$m-$d $h" for timestamp comparison. Not using DateTime - // formatting since package:intl is not available. - notifyLocalTime(timeStr.split(":")[0]); + notifyLocalTime(localTimeAsString()); +} + +@pragma('vm:entry-point') +void timezonesChange() { + do { + notifyLocalTime(localTimeAsString()); + } while (waitFixture()); } void notifyCanAccessResource(bool success) native 'NotifyCanAccessResource'; diff --git a/engine/src/flutter/shell/common/shell_fuchsia_unittests.cc b/engine/src/flutter/shell/common/shell_fuchsia_unittests.cc new file mode 100644 index 00000000000..13f544be96b --- /dev/null +++ b/engine/src/flutter/shell/common/shell_fuchsia_unittests.cc @@ -0,0 +1,257 @@ +// 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. + +// A fuchsia-specific shell test. +// +// This test is only supposed to be ran on Fuchsia OS, as it exercises +// Fuchsia-specific functionality for which no equivalent exists elsewhere. + +#define FML_USED_ON_EMBEDDER + +#include +#include + +#include + +#include +#include +#include + +#include "flutter/fml/dart/dart_converter.h" +#include "flutter/fml/logging.h" +#include "flutter/fml/synchronization/count_down_latch.h" +#include "flutter/runtime/dart_vm.h" +#include "flutter/shell/common/shell_test.h" + +namespace flutter { +namespace testing { + +using fuchsia::intl::TimeZoneId; +using fuchsia::settings::Intl_Set_Result; +using fuchsia::settings::IntlSettings; + +class FuchsiaShellTest : public ShellTest { + protected: + FuchsiaShellTest() + : ctx_(sys::ComponentContext::CreateAndServeOutgoingDirectory()), + intl_() { + ctx_->svc()->Connect(intl_.NewRequest()); + } + + ~FuchsiaShellTest() { + // Restore the time zone that matche that of the test harness. This is + // the default. + const std::string local_timezone = GetLocalTimezone(); + SetTimezone(local_timezone); + AssertTimezone(local_timezone, GetSettings()); + } + + // Gets the international settings from this Fuchsia realm. + IntlSettings GetSettings() { + IntlSettings settings; + zx_status_t status = intl_->Watch2(&settings); + EXPECT_EQ(status, ZX_OK); + return settings; + } + + // Sets the timezone of this Fuchsia realm to `timezone_name`. + void SetTimezone(const std::string& timezone_name) { + fuchsia::settings::IntlSettings settings; + settings.set_time_zone_id(TimeZoneId{.id = timezone_name}); + Intl_Set_Result result; + zx_status_t status = intl_->Set(std::move(settings), &result); + ASSERT_EQ(status, ZX_OK); + } + + std::string GetLocalTimezone() { + const time_t timestamp = time(nullptr); + const struct tm* local_time = localtime(×tamp); + EXPECT_NE(local_time, nullptr) + << "Could not get local time: errno=" << errno << ": " + << strerror(errno); + return std::string(local_time->tm_zone); + } + + std::string GetLocalTime() { + const time_t timestamp = time(nullptr); + const struct tm* local_time = localtime(×tamp); + EXPECT_NE(local_time, nullptr) + << "Could not get local time: errno=" << errno << ": " + << strerror(errno); + char buffer[sizeof("2020-08-26 14")]; + const size_t written = + strftime(buffer, sizeof(buffer), "%Y-%m-%d %H", local_time); + EXPECT_LT(0UL, written); + return std::string(buffer); + } + + // Checks that the timezone name in the `settings` matches what is `expected`. + void AssertTimezone(const std::string& expected, + const IntlSettings& settings) { + ASSERT_EQ(expected, settings.time_zone_id().id); + } + + std::unique_ptr ctx_; + fuchsia::settings::IntlSyncPtr intl_; + + fuchsia::settings::IntlSettings save_settings_; +}; + +static bool ValidateShell(Shell* shell) { + if (!shell) { + return false; + } + + if (!shell->IsSetup()) { + return false; + } + + ShellTest::PlatformViewNotifyCreated(shell); + + { + fml::AutoResetWaitableEvent latch; + fml::TaskRunner::RunNowOrPostTask( + shell->GetTaskRunners().GetPlatformTaskRunner(), [shell, &latch]() { + shell->GetPlatformView()->NotifyDestroyed(); + latch.Signal(); + }); + latch.Wait(); + } + + return true; +} + +// Runs the function `f` in lock-step with a Dart isolate until the function +// returns `true`, or until a certain fixed number of retries is exhausted. +// Events `tick` and `tock` are used to synchronize the lock-stepping. The +// event 'tick' is a signal to the dart isolate to advance a single iteration +// step. 'tock' is used by the dart isolate to signal that it has completed +// its step. +static void RunCoroutineWithRetry(int retries, + fml::AutoResetWaitableEvent* tick, + fml::AutoResetWaitableEvent* tock, + std::function f) { + for (; retries > 0; retries--) { + // Do a single coroutine step. + tick->Signal(); + tock->Wait(); + if (f()) { + break; + } + FML_LOG(INFO) << "Retries left: " << retries; + sleep(1); + } +} + +// Verifies that changing the Fuchsia settings timezone through the FIDL +// settings interface results in a change of the reported local time in the +// isolate. +// +// The test is as follows: +// +// - Set an initial timezone, then get a timestamp from the isolate rounded down +// to the nearest hour. The assumption is as long as this test doesn't run +// very near the whole hour, which should be very unlikely, the nearest hour +// will vary depending on the time zone. +// - Set a different timezone. Get a timestamp from the isolate again and +// confirm that this time around the timestamps are different. +// - Set the initial timezone again, and get the timestamp. This time, the +// timestamp rounded down to whole hour should match the timestamp we got +// in the initial step. +TEST_F(FuchsiaShellTest, LocaltimesVaryOnTimezoneChanges) { + // See fixtures/shell_test.dart, the callback NotifyLocalTime is declared + // there. + fml::AutoResetWaitableEvent latch; + std::string dart_isolate_time_str; + AddNativeCallback("NotifyLocalTime", CREATE_NATIVE_ENTRY([&](auto args) { + dart_isolate_time_str = + tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0)); + latch.Signal(); + })); + + // As long as this is set, the isolate will keep rerunning its only task. + bool continue_fixture = true; + fml::AutoResetWaitableEvent fixture_latch; + AddNativeCallback("WaitFixture", CREATE_NATIVE_ENTRY([&](auto args) { + // Wait for the test fixture to advance. + fixture_latch.Wait(); + tonic::DartConverter::SetReturnValue( + args, continue_fixture); + })); + + auto settings = CreateSettingsForFixture(); + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("timezonesChange"); + + std::unique_ptr shell = CreateShell(settings); + ASSERT_NE(shell.get(), nullptr); + ASSERT_TRUE(ValidateShell(shell.get())); + RunEngine(shell.get(), std::move(configuration)); + latch.Wait(); // After this point, the fixture is at waitFixture(). + + // Start with the local timezone, ensure that the isolate and the test + // fixture are the same. + SetTimezone(GetLocalTimezone()); + AssertTimezone(GetLocalTimezone(), GetSettings()); + std::string expected = GetLocalTime(); + std::string actual = "undefined"; + RunCoroutineWithRetry(10, &fixture_latch, &latch, [&]() { + actual = dart_isolate_time_str; + FML_LOG(INFO) << "reference: " << expected << ", actual: " << actual; + return expected == actual; + }); + ASSERT_EQ(expected, actual) + << "The Dart isolate was expected to show the same time as the test " + << "fixture eventually, but that didn't happen after multiple retries."; + + // Set a new timezone, which is hopefully different from the local one. + SetTimezone("America/New_York"); + AssertTimezone("America/New_York", GetSettings()); + RunCoroutineWithRetry(10, &fixture_latch, &latch, [&]() { + actual = dart_isolate_time_str; + FML_LOG(INFO) << "reference: " << expected << ", actual: " << actual; + return expected != actual; + }); + ASSERT_NE(expected, actual) + << "The Dart isolate was expected to show a time different from the test " + << "fixture eventually, but that didn't happen after multiple retries."; + + // Set a new isolate timezone, and check that the reported time is eventually + // different from what it used to be prior to the change. + SetTimezone("Europe/Amsterdam"); + AssertTimezone("Europe/Amsterdam", GetSettings()); + RunCoroutineWithRetry(10, &fixture_latch, &latch, [&]() { + actual = dart_isolate_time_str; + FML_LOG(INFO) << "reference: " << expected << ", actual: " << actual; + return expected != actual; + }); + ASSERT_NE(expected, actual) + << "The Dart isolate was expected to show a time different from the " + << "prior timezone eventually, but that didn't happen after multiple " + << "retries."; + + // Let's try to bring the timezone back to the old one. + expected = actual; + SetTimezone("America/New_York"); + AssertTimezone("America/New_York", GetSettings()); + RunCoroutineWithRetry(10, &fixture_latch, &latch, [&]() { + actual = dart_isolate_time_str; + FML_LOG(INFO) << "reference: " << expected << ", actual: " << actual; + return expected != actual; + }); + ASSERT_NE(expected, actual) + << "The Dart isolate was expected to show a time different from the " + << "prior timezone eventually, but that didn't happen after multiple " + << "retries."; + + // Tell the isolate to exit its loop. + ASSERT_FALSE(fixture_latch.IsSignaledForTest()); + continue_fixture = false; + fixture_latch.Signal(); + DestroyShell(std::move(shell)); +} + +} // namespace testing +} // namespace flutter diff --git a/engine/src/flutter/testing/fuchsia/meta/fuchsia_test.cmx b/engine/src/flutter/testing/fuchsia/meta/fuchsia_test.cmx index c1f1fa71c4d..ad0aa3783ab 100644 --- a/engine/src/flutter/testing/fuchsia/meta/fuchsia_test.cmx +++ b/engine/src/flutter/testing/fuchsia/meta/fuchsia_test.cmx @@ -11,14 +11,15 @@ ], "services": [ "fuchsia.accessibility.semantics.SemanticsManager", - "fuchsia.process.Launcher", "fuchsia.deprecatedtimezone.Timezone", - "fuchsia.netstack.Netstack", - "fuchsia.sysmem.Allocator", - "fuchsia.vulkan.loader.Loader", + "fuchsia.intl.PropertyProvider", "fuchsia.logger.LogSink", + "fuchsia.netstack.Netstack", + "fuchsia.process.Launcher", + "fuchsia.settings.Intl", + "fuchsia.sysmem.Allocator", "fuchsia.tracing.provider.Registry", - "fuchsia.intl.PropertyProvider" + "fuchsia.vulkan.loader.Loader" ] } }