From 060bc69d8bdfe9860a71109ecf92810beb99f4da Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Tue, 17 Dec 2013 21:59:43 -0600 Subject: add clock + tests --- include/datetime/clock-mock.h | 63 +++++++++++ include/datetime/clock.h | 99 +++++++++++++++++ src/clock-live.cpp | 137 ++++++++++++++++++++++++ src/clock.cpp | 98 +++++++++++++++++ tests/test-clock.cc | 244 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 641 insertions(+) create mode 100644 include/datetime/clock-mock.h create mode 100644 include/datetime/clock.h create mode 100644 src/clock-live.cpp create mode 100644 src/clock.cpp create mode 100644 tests/test-clock.cc diff --git a/include/datetime/clock-mock.h b/include/datetime/clock-mock.h new file mode 100644 index 0000000..814b29a --- /dev/null +++ b/include/datetime/clock-mock.h @@ -0,0 +1,63 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Charles Kerr + */ + +#ifndef INDICATOR_DATETIME_CLOCK_MOCK_H +#define INDICATOR_DATETIME_CLOCK_MOCK_H + +#include + +namespace unity { +namespace indicator { +namespace datetime { + +/*** +**** +***/ + +class MockClock: public Clock +{ +public: + + MockClock(GDateTime * dt) { setLocaltime(dt); } + + ~MockClock() { + g_clear_pointer(&localtime_, g_date_time_unref); + } + + GDateTime* localtime() const { + g_assert (localtime_ != nullptr); + return g_date_time_ref(localtime_); + } + + void setLocaltime(GDateTime* dt) { + g_clear_pointer(&localtime_, g_date_time_unref); + localtime_ = g_date_time_ref(dt); + skewDetected(); + } + +private: + + GDateTime * localtime_ = nullptr; +}; + +} // namespace datetime +} // namespace indicator +} // namespace unity + +#endif // INDICATOR_DATETIME_CLOCK_MOCK_H diff --git a/include/datetime/clock.h b/include/datetime/clock.h new file mode 100644 index 0000000..978be27 --- /dev/null +++ b/include/datetime/clock.h @@ -0,0 +1,99 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Charles Kerr + */ + +#ifndef INDICATOR_DATETIME_CLOCK_H +#define INDICATOR_DATETIME_CLOCK_H + +#include + +#include +#include + +#include +#include + +#include +#include + +namespace unity { +namespace indicator { +namespace datetime { + +/** + * \brief A clock. + * + * Provides a signal to notify when clock skew is detected, such as + * when the timezone changes or when the system resumes from sleep. + */ +class Clock +{ +public: + virtual ~Clock(); + virtual GDateTime* localtime() const = 0; + core::Property > timezones; + core::Signal<> skewDetected; + +protected: + Clock(); + +private: + static void onSystemBusReady(GObject*, GAsyncResult*, gpointer); + static void onPrepareForSleep(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar*, GVariant*, gpointer); + + Clock(const Clock&) =delete; + Clock& operator=(const Clock&) =delete; + + GCancellable * cancellable_ = nullptr; + GDBusConnection * system_bus_ = nullptr; + unsigned int sleep_subscription_id_ = 0; +}; + +/*** +**** +***/ + +/** + * \brief A live clock that provides the actual system time. + * + * Adds another clock skew detection test: wakes up every + * skewTestIntervalSec seconds to see how much time has passed + * since the last time it checked. + */ +class LiveClock: public Clock +{ +public: + LiveClock (const std::shared_ptr& zones); + virtual ~LiveClock(); + virtual GDateTime* localtime() const; + core::Property skewTestIntervalSec; + +private: + class Impl; + std::unique_ptr p; +}; + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity + +#endif // INDICATOR_DATETIME_CLOCK_H diff --git a/src/clock-live.cpp b/src/clock-live.cpp new file mode 100644 index 0000000..80e91a3 --- /dev/null +++ b/src/clock-live.cpp @@ -0,0 +1,137 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Charles Kerr + */ + +#include + +namespace unity { +namespace indicator { +namespace datetime { + +class LiveClock::Impl +{ +public: + + Impl(LiveClock& owner, const std::shared_ptr& tzd): + owner_(owner), + timezones_(tzd) + { + if (timezones_) + { + timezones_->timezone.changed().connect ([this](const std::string& z) {setTimezone(z);}); + setTimezone(timezones_->timezone.get()); + } + + owner_.skewTestIntervalSec.changed().connect([this](unsigned int intervalSec) {setInterval(intervalSec);}); + setInterval(owner_.skewTestIntervalSec.get()); + } + + ~Impl() + { + clearTimer(); + + g_clear_pointer (&timezone_, g_time_zone_unref); + } + + GDateTime* localtime() const + { + g_assert (timezone_ != nullptr); + + return g_date_time_new_now (timezone_); + } + +private: + + void setTimezone (const std::string& str) + { + g_clear_pointer (&timezone_, g_time_zone_unref); + timezone_= g_time_zone_new (str.c_str()); + owner_.skewDetected(); + } + +private: + + void clearTimer() + { + if (skew_timeout_id_) + { + g_source_remove(skew_timeout_id_); + skew_timeout_id_ = 0; + } + + g_clear_pointer(&prev_datetime_, g_date_time_unref); + } + + void setInterval(unsigned int seconds) + { + clearTimer(); + + if (seconds > 0) + { + prev_datetime_ = owner_.localtime(); + skew_timeout_id_ = g_timeout_add_seconds(seconds, onTimerPulse, this); + } + } + + static gboolean onTimerPulse(gpointer gself) + { + auto self = static_cast(gself); + + // check to see if too much time passed since the last check */ + GDateTime * now = self->owner_.localtime(); + const GTimeSpan diff = g_date_time_difference(now, self->prev_datetime_); + const GTimeSpan fuzz = 5; + const GTimeSpan max = (self->owner_.skewTestIntervalSec.get() + fuzz) * G_USEC_PER_SEC; + if (abs(diff) > max) + self->owner_.skewDetected(); + + // update prev_datetime + g_clear_pointer(&self->prev_datetime_, g_date_time_unref); + self->prev_datetime_ = now; + + return G_SOURCE_CONTINUE; + } + +protected: + + LiveClock& owner_; + GTimeZone * timezone_ = nullptr; + std::shared_ptr timezones_; + + GDateTime * prev_datetime_ = nullptr; + unsigned int skew_timeout_id_ = 0; + unsigned int sleep_subscription_id_ = 0; +}; + +LiveClock::LiveClock(const std::shared_ptr& tzd): + p (new Impl (*this, tzd)) +{ +} + +LiveClock::~LiveClock() =default; + +GDateTime * +LiveClock::localtime() const +{ + return p->localtime(); +} + +} // namespace datetime +} // namespace indicator +} // namespace unity + diff --git a/src/clock.cpp b/src/clock.cpp new file mode 100644 index 0000000..4a75ceb --- /dev/null +++ b/src/clock.cpp @@ -0,0 +1,98 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Charles Kerr + */ + +#include + +#include +#include + +namespace unity { +namespace indicator { +namespace datetime { + +/*** +**** +***/ + +Clock::Clock(): + cancellable_(g_cancellable_new()) +{ + g_bus_get(G_BUS_TYPE_SYSTEM, cancellable_, onSystemBusReady, this); + + timezones.changed().connect([this](const std::set& timezones){ + g_message ("timezones changed... new count is %d", (int)timezones.size()); + skewDetected(); + }); +} + +Clock::~Clock() +{ + g_cancellable_cancel(cancellable_); + g_clear_object(&cancellable_); + + if (sleep_subscription_id_) + g_dbus_connection_signal_unsubscribe(system_bus_ , sleep_subscription_id_); + + g_clear_object(&system_bus_); +} + +void +Clock::onSystemBusReady(GObject*, GAsyncResult * res, gpointer gself) +{ + GDBusConnection * system_bus; + + if ((system_bus = g_bus_get_finish(res, nullptr))) + { + auto self = static_cast(gself); + + self->system_bus_ = system_bus; + + self->sleep_subscription_id_ = g_dbus_connection_signal_subscribe( + system_bus, + nullptr, + "org.freedesktop.login1.Manager", // interface + "PrepareForSleep", // signal name + "/org/freedesktop/login1", // object path + nullptr, // arg0 + G_DBUS_SIGNAL_FLAGS_NONE, + onPrepareForSleep, + self, + nullptr); + } +} + +void +Clock::onPrepareForSleep(GDBusConnection * connection G_GNUC_UNUSED, + const gchar * sender_name G_GNUC_UNUSED, + const gchar * object_path G_GNUC_UNUSED, + const gchar * interface_name G_GNUC_UNUSED, + const gchar * signal_name G_GNUC_UNUSED, + GVariant * parameters G_GNUC_UNUSED, + gpointer gself) +{ + static_cast(gself)->skewDetected(); +} + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity diff --git a/tests/test-clock.cc b/tests/test-clock.cc new file mode 100644 index 0000000..a0b4360 --- /dev/null +++ b/tests/test-clock.cc @@ -0,0 +1,244 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Charles Kerr + */ + +#include +#include + +#include "glib-fixture.h" + +/*** +**** +***/ + +using unity::indicator::datetime::Clock; +using unity::indicator::datetime::LiveClock; +using unity::indicator::datetime::Timezones; + +class ClockFixture: public GlibFixture +{ + private: + + typedef GlibFixture super; + + static void + on_bus_opened (GObject * o G_GNUC_UNUSED, GAsyncResult * res, gpointer gself) + { + auto self = static_cast(gself); + + GError * err = 0; + self->system_bus = g_bus_get_finish (res, &err); + g_assert_no_error (err); + + g_main_loop_quit (self->loop); + } + + static void + on_bus_closed (GObject * o G_GNUC_UNUSED, GAsyncResult * res, gpointer gself) + { + auto self = static_cast(gself); + + GError * err = 0; + g_dbus_connection_close_finish (self->system_bus, res, &err); + g_assert_no_error (err); + + g_main_loop_quit (self->loop); + } + + protected: + + GTestDBus * test_dbus; + GDBusConnection * system_bus; + + virtual void SetUp () + { + super::SetUp (); + + // pull up a test dbus + test_dbus = g_test_dbus_new (G_TEST_DBUS_NONE); + g_test_dbus_up (test_dbus); + const char * address = g_test_dbus_get_bus_address (test_dbus); + g_setenv ("DBUS_SYSTEM_BUS_ADDRESS", address, TRUE); + g_debug ("test_dbus's address is %s", address); + + // wait for the GDBusConnection before returning + g_bus_get (G_BUS_TYPE_SYSTEM, nullptr, on_bus_opened, this); + g_main_loop_run (loop); + } + + virtual void TearDown () + { + // close the system bus + g_dbus_connection_close (system_bus, nullptr, on_bus_closed, this); + g_main_loop_run (loop); + g_clear_object (&system_bus); + + // tear down the test dbus + g_test_dbus_down (test_dbus); + g_clear_object (&test_dbus); + + super::TearDown (); + } + + public: + + void emitPrepareForSleep () + { + g_dbus_connection_emit_signal (g_bus_get_sync (G_BUS_TYPE_SYSTEM, nullptr, nullptr), + NULL, + "/org/freedesktop/login1", // object path + "org.freedesktop.login1.Manager", // interface + "PrepareForSleep", // signal name + g_variant_new("(b)", FALSE), + NULL); + } +}; + +/*** +**** +***/ + +#define TIMEZONE_FILE (SANDBOX"/timezone") + +TEST_F (ClockFixture, HelloFixture) +{ + std::shared_ptr zones (new Timezones); + zones->timezone.set("America/New_York"); + LiveClock clock (zones); + +#if 0 + GTimeZone * tz_nyc = g_time_zone_new (file_timezone.c_str()); + GDateTime * now_nyc = g_date_time_new_now (tz_nyc); + GDateTime * now = clock.localtime(); + EXPECT_EQ (g_date_time_get_utc_offset(now_nyc), g_date_time_get_utc_offset(now)); + EXPECT_LE (abs(g_date_time_difference(now_nyc,now)), G_USEC_PER_SEC); + g_date_time_unref (now); + g_date_time_unref (now_nyc); + g_time_zone_unref (tz_nyc); + + /// change the timezones! + clock.skewDetected.connect([this](){ + g_main_loop_quit(loop); + }); + file_timezone = "America/Los_Angeles"; + g_idle_add ([](gpointer str){ + set_file(static_cast(str)); + return G_SOURCE_REMOVE; + }, const_cast(file_timezone.c_str())); + g_main_loop_run (loop); + + GTimeZone * tz_la = g_time_zone_new (file_timezone.c_str()); + GDateTime * now_la = g_date_time_new_now (tz_la); + now = clock.localtime(); + EXPECT_EQ (g_date_time_get_utc_offset(now_la), g_date_time_get_utc_offset(now)); + EXPECT_LE (abs(g_date_time_difference(now_la,now)), G_USEC_PER_SEC); + g_date_time_unref (now); + g_date_time_unref (now_la); + g_time_zone_unref (tz_la); +#endif +} + + +TEST_F (ClockFixture, TimezoneChangeTriggersSkew) +{ + std::shared_ptr zones (new Timezones); + zones->timezone.set("America/New_York"); + LiveClock clock (zones); + //std::string file_timezone = "America/New_York"; + //set_file (file_timezone); + //std::shared_ptr detector (new TimezoneDetector(TIMEZONE_FILE)); + //LiveClock clock (detector); + + GTimeZone * tz_nyc = g_time_zone_new ("America/New_York"); + GDateTime * now_nyc = g_date_time_new_now (tz_nyc); + GDateTime * now = clock.localtime(); + EXPECT_EQ (g_date_time_get_utc_offset(now_nyc), g_date_time_get_utc_offset(now)); + EXPECT_LE (abs(g_date_time_difference(now_nyc,now)), G_USEC_PER_SEC); + g_date_time_unref (now); + g_date_time_unref (now_nyc); + g_time_zone_unref (tz_nyc); + + /// change the timezones! + clock.skewDetected.connect([this](){ + g_main_loop_quit(loop); + }); + g_idle_add ([](gpointer gs){ + static_cast(gs)->timezone.set("America/Los_Angeles"); + return G_SOURCE_REMOVE; + }, zones.get()); + g_main_loop_run (loop); + + GTimeZone * tz_la = g_time_zone_new ("America/Los_Angeles"); + GDateTime * now_la = g_date_time_new_now (tz_la); + now = clock.localtime(); + EXPECT_EQ (g_date_time_get_utc_offset(now_la), g_date_time_get_utc_offset(now)); + EXPECT_LE (abs(g_date_time_difference(now_la,now)), G_USEC_PER_SEC); + g_date_time_unref (now); + g_date_time_unref (now_la); + g_time_zone_unref (tz_la); +} + +/** + * Confirm that a "PrepareForSleep" event wil trigger a skew event + */ +TEST_F (ClockFixture, SleepTriggersSkew) +{ + std::shared_ptr zones (new Timezones); + zones->timezone.set("America/New_York"); + LiveClock clock (zones); + wait_msec (500); // wait for the bus to set up + + bool skewed = false; + clock.skewDetected.connect([&skewed, this](){ + skewed = true; + g_main_loop_quit(loop); + return G_SOURCE_REMOVE; + }); + + g_idle_add ([](gpointer gself){ + static_cast(gself)->emitPrepareForSleep(); + return G_SOURCE_REMOVE; + }, this); + + wait_msec (1000); + EXPECT_TRUE(skewed); +} + +/** + * Confirm that normal time passing doesn't trigger a skew event. + * that idling changing the clock's time triggers a skew event + */ +TEST_F (ClockFixture, IdleDoesNotTriggerSkew) +{ + std::shared_ptr zones (new Timezones); + zones->timezone.set("America/New_York"); + LiveClock clock (zones); + wait_msec (500); // wait for the bus to set up + + bool skewed = false; + clock.skewDetected.connect([&skewed](){ + skewed = true; + g_warn_if_reached(); + return G_SOURCE_REMOVE; + }); + + const unsigned int intervalSec = 4; + clock.skewTestIntervalSec.set(intervalSec); + wait_msec (intervalSec * 2.5 * 1000); + EXPECT_FALSE (skewed); +} -- cgit v1.2.3