diff options
17 files changed, 1123 insertions, 255 deletions
diff --git a/debian/changelog b/debian/changelog index 811e854..6983fe9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,18 @@ +indicator-datetime (13.10.0+15.10.20150624-0ubuntu1) wily; urgency=medium + + [ Charles Kerr ] + * Speedups/cleanups to the EDS regression tests + + -- CI Train Bot <ci-train-bot@canonical.com> Wed, 24 Jun 2015 12:51:27 +0000 + +indicator-datetime (13.10.0+15.10.20150622-0ubuntu1) wily; urgency=medium + + [ Charles Kerr ] + * Fix bugs relating to timezones and triggers from clock-app alarms. + (LP: #1456281, #1465806) + + -- CI Train Bot <ci-train-bot@canonical.com> Mon, 22 Jun 2015 18:03:38 +0000 + indicator-datetime (13.10.0+15.10.20150515-0ubuntu1) wily; urgency=medium [ Charles Kerr ] diff --git a/src/engine-eds.cpp b/src/engine-eds.cpp index f1c691e..e963921 100644 --- a/src/engine-eds.cpp +++ b/src/engine-eds.cpp @@ -34,9 +34,11 @@ namespace unity { namespace indicator { namespace datetime { -static constexpr char const * TAG_ALARM {"x-canonical-alarm"}; +static constexpr char const * TAG_ALARM {"x-canonical-alarm"}; static constexpr char const * TAG_DISABLED {"x-canonical-disabled"}; +static constexpr char const * X_PROP_ACTIVATION_URL {"X-CANONICAL-ACTIVATION-URL"}; + /**** ***** ****/ @@ -77,9 +79,6 @@ public: const Timezone& timezone, std::function<void(const std::vector<Appointment>&)> func) { - const auto begin_timet = begin.to_unix(); - const auto end_timet = end.to_unix(); - const auto b_str = begin.format("%F %T"); const auto e_str = end.format("%F %T"); g_debug("getting all appointments from [%s ... %s]", b_str.c_str(), e_str.c_str()); @@ -104,42 +103,51 @@ public: *** walk through the sources to build the appointment list **/ - auto task_deleter = [](Task* task){ - // give the caller the (sorted) finished product - auto& a = task->appointments; - std::sort(a.begin(), a.end(), [](const Appointment& a, const Appointment& b){return a.begin < b.begin;}); - task->func(a); - // we're done; delete the task - g_debug("time to delete task %p", (void*)task); - delete task; - }; - - std::shared_ptr<Task> main_task(new Task(this, func), task_deleter); + auto gtz = default_timezone != nullptr + ? g_time_zone_new(icaltimezone_get_location(default_timezone)) + : g_time_zone_new_local(); + auto main_task = std::make_shared<Task>(this, func, default_timezone, gtz, begin, end); for (auto& kv : m_clients) { auto& client = kv.second; if (default_timezone != nullptr) e_cal_client_set_default_timezone(client, default_timezone); + g_debug("calling e_cal_client_generate_instances for %p", (void*)client); - // start a new subtask to enumerate all the components in this client. auto& source = kv.first; auto extension = e_source_get_extension(source, E_SOURCE_EXTENSION_CALENDAR); const auto color = e_source_selectable_get_color(E_SOURCE_SELECTABLE(extension)); - g_debug("calling e_cal_client_generate_instances for %p", (void*)client); - auto subtask = new AppointmentSubtask(main_task, - client, - color, - default_timezone, - begin_timet, - end_timet); - e_cal_client_generate_instances(client, - begin_timet, - end_timet, - m_cancellable, - my_get_appointments_foreach, - subtask, - [](gpointer g){delete static_cast<AppointmentSubtask*>(g);}); + + auto begin_str = isodate_from_time_t(begin.to_unix()); + auto end_str = isodate_from_time_t(end.to_unix()); + auto sexp_fmt = g_strdup_printf("(%%s? (make-time \"%s\") (make-time \"%s\"))", begin_str, end_str); + g_clear_pointer(&begin_str, g_free); + g_clear_pointer(&end_str, g_free); + + // ask EDS about alarms that occur in this window... + auto sexp = g_strdup_printf(sexp_fmt, "has-alarms-in-range"); + g_debug("%s alarm sexp is %s", G_STRLOC, sexp); + e_cal_client_get_object_list_as_comps( + client, + sexp, + m_cancellable, + on_alarm_component_list_ready, + new ClientSubtask(main_task, client, color)); + g_clear_pointer(&sexp, g_free); + + // ask EDS about events that occur in this window... + sexp = g_strdup_printf(sexp_fmt, "occur-in-time-range"); + g_debug("%s event sexp is %s", G_STRLOC, sexp); + e_cal_client_get_object_list_as_comps( + client, + sexp, + m_cancellable, + on_event_component_list_ready, + new ClientSubtask(main_task, client, color)); + g_clear_pointer(&sexp, g_free); + + g_clear_pointer(&sexp_fmt, g_free); } } @@ -290,10 +298,14 @@ private: // add the client to our collection auto self = static_cast<Impl*>(gself); g_debug("got a client for %s", e_cal_client_get_local_attachment_store(E_CAL_CLIENT(client))); - self->m_clients[e_client_get_source(client)] = E_CAL_CLIENT(client); + auto source = e_client_get_source(client); + auto ecc = E_CAL_CLIENT(client); + self->m_clients[source] = ecc; + + self->ensure_client_alarms_have_triggers(ecc); // now create a view for it so that we can listen for changes - e_cal_client_get_view (E_CAL_CLIENT(client), + e_cal_client_get_view (ecc, "#t", // match all self->m_cancellable, on_client_view_ready, @@ -402,36 +414,180 @@ private: static_cast<Impl*>(gself)->set_dirty_soon(); } + /*** + **** + ***/ + + // old ubuntu-clock-app alarms created VTODO VALARMS without the + // required 'TRIGGER' property... http://pad.lv/1465806 + + void ensure_client_alarms_have_triggers(ECalClient* client) + { + // ask the EDS server for all the ubuntu-clock-app alarms... + + auto sexp = g_strdup_printf("has-categories? '%s'", TAG_ALARM); + + e_cal_client_get_object_list_as_comps( + client, + sexp, + m_cancellable, + ensure_client_alarms_have_triggers_async_cb, + this); + + g_clear_pointer(&sexp, g_free); + } + + static void ensure_client_alarms_have_triggers_async_cb( + GObject * oclient, + GAsyncResult * res, + gpointer gself) + { + ECalClient * client = E_CAL_CLIENT(oclient); + GError * error = nullptr; + GSList * components = nullptr; + + if (e_cal_client_get_object_list_as_comps_finish(client, + res, + &components, + &error)) + { + auto self = static_cast<Impl*>(gself); + self->ensure_canonical_alarms_have_triggers(client, components); + e_cal_client_free_ecalcomp_slist(components); + } + else if (error != nullptr) + { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning("can't get clock-app alarm list: %s", error->message); + + g_error_free(error); + } + } + + void ensure_canonical_alarms_have_triggers(ECalClient * client, + GSList * components) + { + GSList * modify_slist = nullptr; + + // for each component.. + for (auto l=components; l!=nullptr; l=l->next) + { + bool changed = false; + + // for each alarm... + auto component = E_CAL_COMPONENT(l->data); + auto auids = e_cal_component_get_alarm_uids(component); + for(auto l=auids; l!=nullptr; l=l->next) + { + auto auid = static_cast<const char*>(l->data); + auto alarm = e_cal_component_get_alarm(component, auid); + if (alarm == nullptr) + continue; + + // if the alarm has no trigger, add one. + ECalComponentAlarmTrigger trigger; + e_cal_component_alarm_get_trigger(alarm, &trigger); + if (trigger.type == E_CAL_COMPONENT_ALARM_TRIGGER_NONE) + { + trigger.type = E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_START; + trigger.u.rel_duration = icaldurationtype_from_int(0); + e_cal_component_alarm_set_trigger (alarm, trigger); + changed = true; + } + + g_clear_pointer(&alarm, e_cal_component_alarm_free); + } + g_clear_pointer(&auids, cal_obj_uid_list_free); + + if (changed) + { + auto icc = e_cal_component_get_icalcomponent(component); // icc owned by ecc + modify_slist = g_slist_prepend(modify_slist, icc); + } + } + + if (modify_slist != nullptr) + { + e_cal_client_modify_objects(client, + modify_slist, + E_CAL_OBJ_MOD_ALL, + m_cancellable, + ensure_canonical_alarms_have_triggers_async_cb, + this); + + g_clear_pointer(&modify_slist, g_slist_free); + } + } + + // log a warning if e_cal_client_modify_objects() failed + static void ensure_canonical_alarms_have_triggers_async_cb( + GObject * oclient, + GAsyncResult * res, + gpointer /*gself*/) + { + GError * error = nullptr; + + e_cal_client_modify_objects_finish (E_CAL_CLIENT(oclient), res, &error); + + if (error != nullptr) + { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning("couldn't add alarm triggers: %s", error->message); + + g_error_free(error); + } + } + + /*** + **** + ***/ + + typedef std::function<void(const std::vector<Appointment>&)> appointment_func; struct Task { Impl* p; appointment_func func; + icaltimezone* default_timezone; // pointer owned by libical + GTimeZone* gtz; std::vector<Appointment> appointments; - Task(Impl* p_in, const appointment_func& func_in): p(p_in), func(func_in) {} + const DateTime begin; + const DateTime end; + + Task(Impl* p_in, + appointment_func func_in, + icaltimezone* tz_in, + GTimeZone* gtz_in, + const DateTime& begin_in, + const DateTime& end_in): + p{p_in}, + func{func_in}, + default_timezone{tz_in}, + gtz{gtz_in}, + begin{begin_in}, + end{end_in} {} + + ~Task() { + g_clear_pointer(>z, g_time_zone_unref); + // give the caller the sorted finished product + auto& a = appointments; + std::sort(a.begin(), a.end(), [](const Appointment& a, const Appointment& b){return a.begin < b.begin;}); + func(a); + }; }; - struct AppointmentSubtask + struct ClientSubtask { std::shared_ptr<Task> task; ECalClient* client; std::string color; - icaltimezone* default_timezone; - time_t begin; - time_t end; - - AppointmentSubtask(const std::shared_ptr<Task>& task_in, - ECalClient* client_in, - const char* color_in, - icaltimezone* default_tz, - time_t begin_, - time_t end_): + + ClientSubtask(const std::shared_ptr<Task>& task_in, + ECalClient* client_in, + const char* color_in): task(task_in), - client(client_in), - default_timezone(default_tz), - begin(begin_), - end(end_) + client(client_in) { if (color_in) color = color_in; @@ -481,146 +637,290 @@ private: return ret; } - static gboolean - my_get_appointments_foreach(ECalComponent* component, - time_t begin, - time_t end, - gpointer gsubtask) + static void + on_alarm_component_list_ready(GObject * oclient, + GAsyncResult * res, + gpointer gsubtask) + { + GError * error = NULL; + GSList * comps_slist = NULL; + auto subtask = static_cast<ClientSubtask*>(gsubtask); + + if (e_cal_client_get_object_list_as_comps_finish(E_CAL_CLIENT(oclient), + res, + &comps_slist, + &error)) + { + // _generate_alarms takes a GList, so make a shallow one + GList * comps_list = nullptr; + for (auto l=comps_slist; l!=nullptr; l=l->next) + comps_list = g_list_prepend(comps_list, l->data); + + constexpr std::array<ECalComponentAlarmAction,1> omit = { + (ECalComponentAlarmAction)-1 + }; // list of action types to omit, terminated with -1 + GSList * comp_alarms = nullptr; + e_cal_util_generate_alarms_for_list( + comps_list, + subtask->task->begin.to_unix(), + subtask->task->end.to_unix(), + const_cast<ECalComponentAlarmAction*>(omit.data()), + &comp_alarms, + e_cal_client_resolve_tzid_cb, + oclient, + subtask->task->default_timezone); + + // walk the alarms & add them + for (auto l=comp_alarms; l!=nullptr; l=l->next) + add_alarms_to_subtask(static_cast<ECalComponentAlarms*>(l->data), subtask, subtask->task->gtz); + + // cleanup + e_cal_free_alarms(comp_alarms); + g_list_free(comps_list); + e_cal_client_free_ecalcomp_slist(comps_slist); + } + else if (error != nullptr) + { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning("can't get ecalcomponent list: %s", error->message); + + g_error_free(error); + } + + delete subtask; + } + + static void + on_event_component_list_ready(GObject * oclient, + GAsyncResult * res, + gpointer gsubtask) + { + GError * error = NULL; + GSList * comps_slist = NULL; + auto subtask = static_cast<ClientSubtask*>(gsubtask); + + if (e_cal_client_get_object_list_as_comps_finish(E_CAL_CLIENT(oclient), + res, + &comps_slist, + &error)) + { + for (auto l=comps_slist; l!=nullptr; l=l->next) + add_event_to_subtask(static_cast<ECalComponent*>(l->data), subtask, subtask->task->gtz); + + e_cal_client_free_ecalcomp_slist(comps_slist); + } + else if (error != nullptr) + { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning("can't get ecalcomponent list: %s", error->message); + + g_error_free(error); + } + + delete subtask; + } + + static DateTime + datetime_from_component_date_time(const ECalComponentDateTime & in, + GTimeZone * default_timezone) + { + DateTime out; + + g_return_val_if_fail(in.value != nullptr, out); + + auto gtz = in.tzid == nullptr ? g_time_zone_ref(default_timezone) + : g_time_zone_new(in.tzid); + out = DateTime(gtz, + in.value->year, + in.value->month, + in.value->day, + in.value->hour, + in.value->minute, + in.value->second); + g_time_zone_unref(gtz); + return out; + } + + static bool + is_component_interesting(ECalComponent * component) { + // we only want calendar events and vtodos const auto vtype = e_cal_component_get_vtype(component); - auto subtask = static_cast<AppointmentSubtask*>(gsubtask); - - if ((vtype == E_CAL_COMPONENT_EVENT) || (vtype == E_CAL_COMPONENT_TODO)) - { - const gchar* uid = nullptr; - e_cal_component_get_uid(component, &uid); - - auto status = ICAL_STATUS_NONE; - e_cal_component_get_status(component, &status); - - // get the timezone we want to use for generated Appointments/Alarms - const char * location = icaltimezone_get_location(subtask->default_timezone); - auto gtz = g_time_zone_new(location); - g_debug("timezone abbreviation is %s", g_time_zone_get_abbreviation (gtz, 0)); - - const DateTime begin_dt { gtz, begin }; - const DateTime end_dt { gtz, end }; - g_debug ("got appointment from %s to %s, uid %s status %d", - begin_dt.format("%F %T %z").c_str(), - end_dt.format("%F %T %z").c_str(), - uid, - (int)status); - - // look for the in-house tags - bool disabled = false; - Appointment::Type type = Appointment::EVENT; - GSList * categ_list = nullptr; - e_cal_component_get_categories_list (component, &categ_list); - for (GSList * l=categ_list; l!=nullptr; l=l->next) { - auto tag = static_cast<const char*>(l->data); - if (!g_strcmp0(tag, TAG_ALARM)) - type = Appointment::UBUNTU_ALARM; - if (!g_strcmp0(tag, TAG_DISABLED)) - disabled = true; + if ((vtype != E_CAL_COMPONENT_EVENT) && + (vtype != E_CAL_COMPONENT_TODO)) + return false; + + // we're not interested in completed or cancelled components + auto status = ICAL_STATUS_NONE; + e_cal_component_get_status(component, &status); + if ((status == ICAL_STATUS_COMPLETED) || + (status == ICAL_STATUS_CANCELLED)) + return false; + + // we don't want disabled alarms + bool disabled = false; + GSList * categ_list = nullptr; + e_cal_component_get_categories_list (component, &categ_list); + for (GSList * l=categ_list; l!=nullptr; l=l->next) { + auto tag = static_cast<const char*>(l->data); + if (!g_strcmp0(tag, TAG_DISABLED)) + disabled = true; + } + e_cal_component_free_categories_list(categ_list); + if (disabled) + return false; + + return true; + } + + static Appointment + get_appointment(ECalComponent * component, GTimeZone * gtz) + { + Appointment baseline; + + // get appointment.uid + const gchar* uid = nullptr; + e_cal_component_get_uid(component, &uid); + if (uid != nullptr) + baseline.uid = uid; + + // get appointment.summary + ECalComponentText text {}; + e_cal_component_get_summary(component, &text); + if (text.value) + baseline.summary = text.value; + + // get appointment.begin + ECalComponentDateTime eccdt_tmp {}; + e_cal_component_get_dtstart(component, &eccdt_tmp); + baseline.begin = datetime_from_component_date_time(eccdt_tmp, gtz); + e_cal_component_free_datetime(&eccdt_tmp); + + // get appointment.end + e_cal_component_get_dtend(component, &eccdt_tmp); + baseline.end = eccdt_tmp.value != nullptr + ? datetime_from_component_date_time(eccdt_tmp, gtz) + : baseline.begin; + e_cal_component_free_datetime(&eccdt_tmp); + + // get appointment.activation_url from x-props + auto icc = e_cal_component_get_icalcomponent(component); // icc owned by component + auto icalprop = icalcomponent_get_first_property(icc, ICAL_X_PROPERTY); + while (icalprop != nullptr) { + const char * x_name = icalproperty_get_x_name(icalprop); + if ((x_name != nullptr) && !g_ascii_strcasecmp(x_name, X_PROP_ACTIVATION_URL)) { + const char * url = icalproperty_get_value_as_string(icalprop); + if ((url != nullptr) && baseline.activation_url.empty()) + baseline.activation_url = url; } - e_cal_component_free_categories_list(categ_list); + icalprop = icalcomponent_get_next_property(icc, ICAL_X_PROPERTY); + } - if ((uid != nullptr) && - (!disabled) && - (status != ICAL_STATUS_COMPLETED) && - (status != ICAL_STATUS_CANCELLED)) - { - constexpr std::array<ECalComponentAlarmAction,1> omit = { (ECalComponentAlarmAction)-1 }; // list of action types to omit, terminated with -1 - Appointment appointment; - - ECalComponentText text {}; - e_cal_component_get_summary(component, &text); - if (text.value) - appointment.summary = text.value; - - appointment.begin = begin_dt; - appointment.end = end_dt; - appointment.color = subtask->color; - appointment.uid = uid; - appointment.type = type; - - icalcomponent * icc = e_cal_component_get_icalcomponent(component); - g_debug("%s", icalcomponent_as_ical_string(icc)); // libical owns this string; no leak - - auto e_alarms = e_cal_util_generate_alarms_for_comp(component, - subtask->begin, - subtask->end, - const_cast<ECalComponentAlarmAction*>(omit.data()), - e_cal_client_resolve_tzid_cb, - subtask->client, - subtask->default_timezone); - - std::map<DateTime,Alarm> alarms; - - if (e_alarms != nullptr) - { - for (auto l=e_alarms->alarms; l!=nullptr; l=l->next) - { - auto ai = static_cast<ECalComponentAlarmInstance*>(l->data); - auto a = e_cal_component_get_alarm(component, ai->auid); - - if (a != nullptr) - { - const DateTime alarm_begin{gtz, ai->trigger}; - auto& alarm = alarms[alarm_begin]; - - if (alarm.text.empty()) - alarm.text = get_alarm_text(a); - if (alarm.audio_url.empty()) - alarm.audio_url = get_alarm_sound_url(a); - if (!alarm.time.is_set()) - alarm.time = alarm_begin; - - e_cal_component_alarm_free(a); - } - } - - e_cal_component_alarms_free(e_alarms); - } - // Hm, no alarm triggers? - // That's a bug in alarms created by some versions of ubuntu-ui-toolkit. - // If that's what's happening here, let's handle those alarms anyway - // by effectively injecting a TRIGGER;VALUE=DURATION;RELATED=START:PT0S - else if (appointment.is_ubuntu_alarm()) - { - Alarm tmp; - tmp.time = appointment.begin; - - auto auids = e_cal_component_get_alarm_uids(component); - for(auto l=auids; l!=nullptr; l=l->next) - { - const auto auid = static_cast<const char*>(l->data); - auto a = e_cal_component_get_alarm(component, auid); - if (a != nullptr) - { - if (tmp.text.empty()) - tmp.text = get_alarm_text(a); - if (tmp.audio_url.empty()) - tmp.audio_url = get_alarm_sound_url(a); - e_cal_component_alarm_free(a); - } - } - cal_obj_uid_list_free(auids); - - alarms[tmp.time] = tmp; - } + // get appointment.type + baseline.type = Appointment::EVENT; + GSList * categ_list = nullptr; + e_cal_component_get_categories_list (component, &categ_list); + for (GSList * l=categ_list; l!=nullptr; l=l->next) { + auto tag = static_cast<const char*>(l->data); + if (!g_strcmp0(tag, TAG_ALARM)) + baseline.type = Appointment::UBUNTU_ALARM; + } + e_cal_component_free_categories_list(categ_list); - appointment.alarms.reserve(alarms.size()); - for (const auto& it : alarms) - appointment.alarms.push_back(it.second); + g_debug("%s got appointment from %s to %s: %s", G_STRLOC, + baseline.begin.format("%F %T %z").c_str(), + baseline.end.format("%F %T %z").c_str(), + icalcomponent_as_ical_string(icc) /* string owned by ical */); - subtask->task->appointments.push_back(appointment); - } + return baseline; + } - g_time_zone_unref(gtz); + static void + add_event_to_subtask(ECalComponent * component, + ClientSubtask * subtask, + GTimeZone * gtz) + { + // events with alarms are covered by add_alarm_to_subtask(), + // so skip them here + auto auids = e_cal_component_get_alarm_uids(component); + const bool has_alarms = auids != nullptr; + cal_obj_uid_list_free(auids); + if (has_alarms) + return; + + // add it. simple, eh? + if (is_component_interesting(component)) + { + Appointment appointment = get_appointment(component, gtz); + appointment.color = subtask->color; + subtask->task->appointments.push_back(appointment); + } + } + + static void + add_alarms_to_subtask(ECalComponentAlarms * comp_alarms, + ClientSubtask * subtask, + GTimeZone * gtz) + { + auto& component = comp_alarms->comp; + + if (!is_component_interesting(component)) + return; + + Appointment baseline = get_appointment(component, gtz); + baseline.color = subtask->color; + + /** + *** Now loop through comp_alarms to get information that we need + *** to build the instance appointments and their alarms. + *** + *** Outer map key is the instance component's start + end time. + *** We build Appointment.begin and .end from that. + *** + *** inner map key is the alarm trigger, we build Alarm.time from that. + *** + *** inner map value is the Alarm. + *** + *** We map the alarms based on their trigger time so that we + *** can fold together multiple valarms that trigger for the + *** same componeng at the same time. This is commonplace; + *** e.g. one valarm will have a display action and another + *** will specify a sound to be played. + */ + std::map<std::pair<DateTime,DateTime>,std::map<DateTime,Alarm>> alarms; + for (auto l=comp_alarms->alarms; l!=nullptr; l=l->next) + { + auto ai = static_cast<ECalComponentAlarmInstance*>(l->data); + auto a = e_cal_component_get_alarm(component, ai->auid); + if (a == nullptr) + continue; + + auto instance_time = std::make_pair(DateTime{gtz, ai->occur_start}, + DateTime{gtz, ai->occur_end}); + auto trigger_time = DateTime{gtz, ai->trigger}; + + auto& alarm = alarms[instance_time][trigger_time]; + + if (alarm.text.empty()) + alarm.text = get_alarm_text(a); + if (alarm.audio_url.empty()) + alarm.audio_url = get_alarm_sound_url(a); + if (!alarm.time.is_set()) + alarm.time = trigger_time; + + e_cal_component_alarm_free(a); + } + + for (auto& i : alarms) + { + Appointment appointment = baseline; + appointment.begin = i.first.first; + appointment.end = i.first.second; + appointment.alarms.reserve(i.second.size()); + for (auto& j : i.second) + appointment.alarms.push_back(j.second); + subtask->task->appointments.push_back(appointment); } - - return G_SOURCE_CONTINUE; } /*** diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1332529..f3ee612 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -70,32 +70,24 @@ target_link_libraries (${TEST_NAME} indicatordatetimeservice gtest ${SERVICE_DEP ## find_program(DBUS_RUNNER dbus-test-runner) -find_program(EVOLUTION_CALENDAR_FACTORY evolution-calendar-factory PATHS /usr/lib/evolution/) -find_program(EVOLUTION_SOURCE_REGISTRY evolution-source-registry PATHS /usr/lib/evolution/) -find_program(GVFSD gvfsd PATHS /usr/lib/gvfs/) -OPTION(EVOLUTION_SOURCE_SERVICE_NAME "DBus name for source registry") -if(NOT EVOLUTION_SOURCE_SERVICE_NAME) - set(EVOLUTION_SOURCE_SERVICE_NAME "org.gnome.evolution.dataserver.Sources4") -endif() -function(add_eds_test_by_name name) +function(add_eds_ics_test_by_name name) set (TEST_NAME ${name}) add_executable(${TEST_NAME} ${TEST_NAME}.cpp gschemas.compiled) target_link_libraries (${TEST_NAME} indicatordatetimeservice gtest ${DBUSTEST_LIBRARIES} ${SERVICE_DEPS_LIBRARIES} ${GTEST_LIBS}) add_test (${TEST_NAME} - ${CMAKE_CURRENT_SOURCE_DIR}/run-eds-test.sh + ${CMAKE_CURRENT_SOURCE_DIR}/run-eds-ics-test.sh ${DBUS_RUNNER} # arg1: dbus-test-runner exec ${CMAKE_CURRENT_BINARY_DIR}/${TEST_NAME} # arg2: test executable path ${TEST_NAME} # arg3: test name - ${EVOLUTION_CALENDAR_FACTORY} # arg4: evolution-calendar-factory exec - ${EVOLUTION_SOURCE_SERVICE_NAME} # arg5: dbus name for source registry - ${EVOLUTION_SOURCE_REGISTRY} # arg6: evolution-source-registry exec - ${GVFSD} # arg7: gvfsd exec - ${CMAKE_CURRENT_SOURCE_DIR}/${TEST_NAME}-config-files) # arg8: canned config files + ${CMAKE_CURRENT_SOURCE_DIR}/test-eds-ics-config-files # arg4: base directory for config file template + ${CMAKE_CURRENT_SOURCE_DIR}/${TEST_NAME}.ics) # arg5: the ical file for this test endfunction() -add_eds_test_by_name(test-eds-valarms) - - +add_eds_ics_test_by_name(test-eds-ics-all-day-events) +add_eds_ics_test_by_name(test-eds-ics-repeating-events) +add_eds_ics_test_by_name(test-eds-ics-nonrepeating-events) +add_eds_ics_test_by_name(test-eds-ics-repeating-valarms) +add_eds_ics_test_by_name(test-eds-ics-missing-trigger) # disabling the timezone unit tests because they require diff --git a/tests/print-to.h b/tests/print-to.h index 78cf574..357925f 100644 --- a/tests/print-to.h +++ b/tests/print-to.h @@ -33,9 +33,42 @@ namespace datetime { ***/ void +PrintTo(const DateTime& datetime, std::ostream* os) +{ + *os << "{time:'" << datetime.format("%F %T %z") << '}'; +} + +void PrintTo(const Alarm& alarm, std::ostream* os) { - *os << "{text:'" << alarm.text << "', audio_url:'" << alarm.audio_url << "', time:'"<<alarm.time.format("%F %T")<<"'}"; + *os << '{'; + *os << "{text:" << alarm.text << '}'; + PrintTo(alarm.time, os); + *os << '}'; +} + +void +PrintTo(const Appointment& appointment, std::ostream* os) +{ + *os << '{'; + + *os << "{uid:'" << appointment.uid << "'}" + << "{color:'" << appointment.color << "'}" + << "{summary:'" << appointment.summary << "'}" + << "{activation_url:'" << appointment.activation_url << "'}"; + + *os << "{begin:"; + PrintTo(appointment.begin, os); + *os << '}'; + + *os << "{end:"; + PrintTo(appointment.end, os); + *os << '}'; + + for(const auto& alarm : appointment.alarms) + PrintTo(alarm, os); + + *os << '}'; } } // namespace datetime diff --git a/tests/run-eds-ics-test.sh b/tests/run-eds-ics-test.sh new file mode 100755 index 0000000..63fc6bc --- /dev/null +++ b/tests/run-eds-ics-test.sh @@ -0,0 +1,66 @@ +#!/bin/sh + +SELF=$0 # this script +TEST_RUNNER=$1 # full executable path of dbus-test-runner +TEST_EXEC=$2 # full executable path of test app +TEST_NAME=$3 # test name +CONFIG_DIR=$4 # config files +ICS_FILE=$5 # ical file holding test data + +echo "this script: ${SELF}" +echo "test-runner: ${TEST_RUNNER}" +echo "test-exec: ${TEST_EXEC}" +echo "test-name: ${TEST_NAME}" +echo "config-dir: ${CONFIG_DIR}" +echo "ics-file: ${ICS_FILE}" + +# set up the tmpdir +export TEST_TMP_DIR=$(mktemp -p "${TMPDIR:-/tmp}" -d ${TEST_NAME}-XXXXXXXXXX) || exit 1 +echo "running test '${TEST_NAME}' in ${TEST_TMP_DIR}" + +# set up the environment variables +export QT_QPA_PLATFORM=minimal +export HOME=${TEST_TMP_DIR} +export XDG_RUNTIME_DIR=${TEST_TMP_DIR} +export XDG_CACHE_HOME=${TEST_TMP_DIR}/.cache +export XDG_CONFIG_HOME=${TEST_TMP_DIR}/.config +export XDG_DATA_HOME=${TEST_TMP_DIR}/.local/share +export XDG_DESKTOP_DIR=${TEST_TMP_DIR} +export XDG_DOCUMENTS_DIR=${TEST_TMP_DIR} +export XDG_DOWNLOAD_DIR=${TEST_TMP_DIR} +export XDG_MUSIC_DIR=${TEST_TMP_DIR} +export XDG_PICTURES_DIR=${TEST_TMP_DIR} +export XDG_PUBLICSHARE_DIR=${TEST_TMP_DIR} +export XDG_TEMPLATES_DIR=${TEST_TMP_DIR} +export XDG_VIDEOS_DIR=${TEST_TMP_DIR} +export QORGANIZER_EDS_DEBUG=On +export GIO_USE_VFS=local # needed to ensure GVFS shuts down cleanly after the test is over + +export G_MESSAGES_DEBUG=all +export G_DBUS_DEBUG=messages + +echo HOMEDIR=${HOME} +rm -rf ${XDG_DATA_HOME} + +# if there are canned config files for this test, move them into place now +if [ -d ${CONFIG_DIR} ]; then + echo "copying files from ${CONFIG_DIR} to $HOME" + cp --verbose --archive ${CONFIG_DIR}/. $HOME +fi + +# if there's a specific ics file to test, copy it on top of the canned config files +if [ -e ${ICS_FILE} ]; then + echo "copying ${ICS_FILE} into $HOME" + cp --verbose --archive ${ICS_FILE} ${XDG_DATA_HOME}/evolution/tasks/system/tasks.ics +fi + +# run the test +${TEST_RUNNER} --keep-env --max-wait=90 --task ${TEST_EXEC} --task-name ${TEST_NAME} --wait-until-complete +rv=$? + +# if the test passed, blow away the tmpdir +if [ $rv -eq 0 ]; then + rm -rf $TEST_TMP_DIR +fi + +return $rv diff --git a/tests/run-eds-test.sh b/tests/run-eds-test.sh deleted file mode 100755 index 1fa28a3..0000000 --- a/tests/run-eds-test.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/sh - -echo ARG0=$0 # this script -echo ARG1=$1 # full executable path of dbus-test-runner -echo ARG2=$2 # full executable path of test app -echo ARG3=$3 # test name -echo ARG4=$4 # full executable path of evolution-calendar-factory -echo ARG5=$5 # bus service name of calendar factory -echo ARG6=$6 # full exectuable path of evolution-source-registry -echo ARG7=$7 # full executable path of gvfs -echo ARG8=$8 # config files - -# set up the tmpdir and tell the shell to purge it when we exit -export TEST_TMP_DIR=$(mktemp -p "${TMPDIR:-/tmp}" -d $3-XXXXXXXXXX) || exit 1 -echo "running test '$3' in ${TEST_TMP_DIR}" - -# set up the environment variables -export QT_QPA_PLATFORM=minimal -export HOME=${TEST_TMP_DIR} -export XDG_RUNTIME_DIR=${TEST_TMP_DIR} -export XDG_CACHE_HOME=${TEST_TMP_DIR}/.cache -export XDG_CONFIG_HOME=${TEST_TMP_DIR}/.config -export XDG_DATA_HOME=${TEST_TMP_DIR}/.local/share -export XDG_DESKTOP_DIR=${TEST_TMP_DIR} -export XDG_DOCUMENTS_DIR=${TEST_TMP_DIR} -export XDG_DOWNLOAD_DIR=${TEST_TMP_DIR} -export XDG_MUSIC_DIR=${TEST_TMP_DIR} -export XDG_PICTURES_DIR=${TEST_TMP_DIR} -export XDG_PUBLICSHARE_DIR=${TEST_TMP_DIR} -export XDG_TEMPLATES_DIR=${TEST_TMP_DIR} -export XDG_VIDEOS_DIR=${TEST_TMP_DIR} -export QORGANIZER_EDS_DEBUG=On -export GIO_USE_VFS=local # needed to ensure GVFS shuts down cleanly after the test is over - -echo HOMEDIR=${HOME} -rm -rf ${XDG_DATA_HOME} - -# if there are canned config files for this test, move them into place now -if [ -d $8 ]; then - echo "copying files from $8 to $HOME" - cp --verbose --archive $8/. $HOME -fi - -# run dbus-test-runner -$1 --keep-env --max-wait=90 \ ---task $2 --task-name $3 --wait-until-complete --wait-for=org.gnome.evolution.dataserver.Calendar7 \ ---task $4 --task-name "evolution" --wait-until-complete -r -#--task $6 --task-name "source-registry" --wait-for=org.gtk.vfs.Daemon -r \ -#--task $7 --task-name "gvfsd" -r -rv=$? - -# if the test passed, blow away the tmpdir -if [ $rv -eq 0 ]; then - rm -rf $TEST_TMP_DIR -fi - -return $rv diff --git a/tests/test-eds-ics-all-day-events.cpp b/tests/test-eds-ics-all-day-events.cpp new file mode 100644 index 0000000..0d3173d --- /dev/null +++ b/tests/test-eds-ics-all-day-events.cpp @@ -0,0 +1,91 @@ +/* + * Copyright 2015 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <algorithm> + +#include <datetime/alarm-queue-simple.h> +#include <datetime/clock-mock.h> +#include <datetime/engine-eds.h> +#include <datetime/planner-range.h> + +#include <gtest/gtest.h> + +#include "glib-fixture.h" +#include "print-to.h" +#include "timezone-mock.h" +#include "wakeup-timer-mock.h" + +using namespace unity::indicator::datetime; +using VAlarmFixture = GlibFixture; + +/*** +**** +***/ + +TEST_F(VAlarmFixture, MultipleAppointments) +{ + // start the EDS engine + auto engine = std::make_shared<EdsEngine>(); + + // we need a consistent timezone for the planner and our local DateTimes + constexpr char const * zone_str {"America/Chicago"}; + auto tz = std::make_shared<MockTimezone>(zone_str); + auto gtz = g_time_zone_new(zone_str); + + // make a planner that looks at the first half of 2015 in EDS + auto planner = std::make_shared<SimpleRangePlanner>(engine, tz); + const DateTime range_begin {gtz, 2015,1, 1, 0, 0, 0.0}; + const DateTime range_end {gtz, 2015,6,31,23,59,59.5}; + planner->range().set(std::make_pair(range_begin, range_end)); + + // give EDS a moment to load + if (planner->appointments().get().empty()) { + g_message("waiting a moment for EDS to load..."); + auto on_appointments_changed = [this](const std::vector<Appointment>& appointments){ + g_message("ah, they loaded"); + if (!appointments.empty()) + g_main_loop_quit(loop); + }; + core::ScopedConnection conn(planner->appointments().changed().connect(on_appointments_changed)); + constexpr int max_wait_sec = 10; + wait_msec(max_wait_sec * G_TIME_SPAN_MILLISECOND); + } + + // what we expect to get... + Appointment expected_appt; + expected_appt.uid = "20150521T111538Z-7449-1000-3572-0@ghidorah"; + expected_appt.color = "#becedd"; + expected_appt.summary = "Memorial Day"; + expected_appt.begin = DateTime{gtz,2015,5,25,0,0,0}; + expected_appt.end = DateTime{gtz,2015,5,26,0,0,0}; + + // compare it to what we actually loaded... + const auto appts = planner->appointments().get(); + ASSERT_EQ(1, appts.size()); + const auto& appt = appts[0]; + EXPECT_EQ(expected_appt.begin, appt.begin); + EXPECT_EQ(expected_appt.end, appt.end); + EXPECT_EQ(expected_appt.uid, appt.uid); + EXPECT_EQ(expected_appt.color, appt.color); + EXPECT_EQ(expected_appt.summary, appt.summary); + EXPECT_EQ(0, appt.alarms.size()); + + // cleanup + g_time_zone_unref(gtz); +} diff --git a/tests/test-eds-ics-all-day-events.ics b/tests/test-eds-ics-all-day-events.ics new file mode 100644 index 0000000..0cf7dab --- /dev/null +++ b/tests/test-eds-ics-all-day-events.ics @@ -0,0 +1,19 @@ +BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
+VERSION:2.0
+X-EVOLUTION-DATA-REVISION:2015-05-07T21:14:49.315443Z(0)
+BEGIN:VEVENT
+UID:20150521T111538Z-7449-1000-3572-0@ghidorah
+DTSTAMP:20150521T001128Z
+DTSTART;VALUE=DATE:20150525
+DTEND;VALUE=DATE:20150526
+TRANSP:TRANSPARENT
+SEQUENCE:2
+SUMMARY:Memorial Day
+DESCRIPTION:Today is Memorial Day
+CLASS:PUBLIC
+CREATED:20150521T111638Z
+LAST-MODIFIED:20150521T111638Z
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/test-eds-valarms-config-files/.config/evolution/sources/system-proxy.source b/tests/test-eds-ics-config-files/.config/evolution/sources/system-proxy.source index 4b2f666..4b2f666 100644 --- a/tests/test-eds-valarms-config-files/.config/evolution/sources/system-proxy.source +++ b/tests/test-eds-ics-config-files/.config/evolution/sources/system-proxy.source diff --git a/tests/test-eds-ics-missing-trigger.cpp b/tests/test-eds-ics-missing-trigger.cpp new file mode 100644 index 0000000..ba87011 --- /dev/null +++ b/tests/test-eds-ics-missing-trigger.cpp @@ -0,0 +1,116 @@ +/* + * Copyright 2015 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <algorithm> + +#include <datetime/alarm-queue-simple.h> +#include <datetime/clock-mock.h> +#include <datetime/engine-eds.h> +#include <datetime/planner-range.h> + +#include <gtest/gtest.h> + +#include "glib-fixture.h" +#include "print-to.h" +#include "timezone-mock.h" +#include "wakeup-timer-mock.h" + +using namespace unity::indicator::datetime; +using VAlarmFixture = GlibFixture; + +/*** +**** +***/ + +TEST_F(VAlarmFixture, MissingTriggers) +{ + // start the EDS engine + auto engine = std::make_shared<EdsEngine>(); + + // we need a consistent timezone for the planner and our local DateTimes + constexpr char const * zone_str {"America/Chicago"}; + auto tz = std::make_shared<MockTimezone>(zone_str); + auto gtz = g_time_zone_new(zone_str); + + // make a planner that looks at the first half of 2015 in EDS + auto planner = std::make_shared<SimpleRangePlanner>(engine, tz); + const DateTime range_begin {gtz, 2015,1, 1, 0, 0, 0.0}; + const DateTime range_end {gtz, 2015,6,31,23,59,59.5}; + planner->range().set(std::make_pair(range_begin, range_end)); + + // give EDS a moment to load + if (planner->appointments().get().empty()) { + g_message("waiting a moment for EDS to load..."); + auto on_appointments_changed = [this](const std::vector<Appointment>& appointments){ + g_message("ah, they loaded"); + if (!appointments.empty()) + g_main_loop_quit(loop); + }; + core::ScopedConnection conn(planner->appointments().changed().connect(on_appointments_changed)); + constexpr int max_wait_sec = 10; + wait_msec(max_wait_sec * G_TIME_SPAN_MILLISECOND); + } + + // build expected: one-time alarm + std::vector<Appointment> expected; + Appointment a; + a.type = Appointment::UBUNTU_ALARM; + a.uid = "20150617T211838Z-6217-32011-2036-1@ubuntu-phablet"; + a.color = "#becedd"; + a.summary = "One Time Alarm"; + a.begin = DateTime { gtz, 2015, 6, 18, 10, 0, 0}; + a.end = a.begin; + a.alarms.resize(1); + a.alarms[0].audio_url = "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"; + a.alarms[0].time = a.begin; + a.alarms[0].text = a.summary; + expected.push_back(a); + + // build expected: recurring alarm + a.uid = "20150617T211913Z-6217-32011-2036-5@ubuntu-phablet"; + a.summary = "Recurring Alarm"; + a.alarms[0].text = a.summary; + std::array<DateTime,14> recurrences { + DateTime{ gtz, 2015, 6, 18, 10, 1, 0 }, + DateTime{ gtz, 2015, 6, 19, 10, 1, 0 }, + DateTime{ gtz, 2015, 6, 20, 10, 1, 0 }, + DateTime{ gtz, 2015, 6, 21, 10, 1, 0 }, + DateTime{ gtz, 2015, 6, 22, 10, 1, 0 }, + DateTime{ gtz, 2015, 6, 23, 10, 1, 0 }, + DateTime{ gtz, 2015, 6, 24, 10, 1, 0 }, + DateTime{ gtz, 2015, 6, 25, 10, 1, 0 }, + DateTime{ gtz, 2015, 6, 26, 10, 1, 0 }, + DateTime{ gtz, 2015, 6, 27, 10, 1, 0 }, + DateTime{ gtz, 2015, 6, 28, 10, 1, 0 }, + DateTime{ gtz, 2015, 6, 29, 10, 1, 0 }, + DateTime{ gtz, 2015, 6, 30, 10, 1, 0 }, + DateTime{ gtz, 2015, 7, 1, 10, 1, 0 } + }; + for (const auto& time : recurrences) { + a.begin = a.end = a.alarms[0].time = time; + expected.push_back(a); + } + + // the planner should match what we've got in the calendar.ics file + const auto appts = planner->appointments().get(); + EXPECT_EQ(expected, appts); + + // cleanup + g_time_zone_unref(gtz); +} diff --git a/tests/test-eds-ics-missing-trigger.ics b/tests/test-eds-ics-missing-trigger.ics new file mode 100644 index 0000000..048e7f2 --- /dev/null +++ b/tests/test-eds-ics-missing-trigger.ics @@ -0,0 +1,45 @@ +BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
+VERSION:2.0
+X-EVOLUTION-DATA-REVISION:2015-06-17T21:19:13.980613Z(3)
+BEGIN:VTODO
+UID:20150617T211838Z-6217-32011-2036-1@ubuntu-phablet
+DTSTAMP:20150617T211838Z
+DTSTART:20150618T100000
+SUMMARY:One Time Alarm
+CATEGORIES:x-canonical-alarm
+CREATED:20150617T211838Z
+LAST-MODIFIED:20150617T211838Z
+BEGIN:VALARM
+X-EVOLUTION-ALARM-UID:20150617T211838Z-6217-32011-2036-2@ubuntu-phablet
+ACTION:AUDIO
+ATTACH:file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg
+END:VALARM
+BEGIN:VALARM
+X-EVOLUTION-ALARM-UID:20150617T211838Z-6217-32011-2036-3@ubuntu-phablet
+ACTION:DISPLAY
+DESCRIPTION:One Time Alarm
+END:VALARM
+END:VTODO
+BEGIN:VTODO
+UID:20150617T211913Z-6217-32011-2036-5@ubuntu-phablet
+DTSTAMP:20150617T211913Z
+DTSTART:20150618T100100
+RRULE:FREQ=DAILY
+SUMMARY:Recurring Alarm
+CATEGORIES:x-canonical-alarm
+CREATED:20150617T211913Z
+LAST-MODIFIED:20150617T211913Z
+BEGIN:VALARM
+X-EVOLUTION-ALARM-UID:20150617T211913Z-6217-32011-2036-6@ubuntu-phablet
+ACTION:AUDIO
+ATTACH:file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg
+END:VALARM
+BEGIN:VALARM
+X-EVOLUTION-ALARM-UID:20150617T211913Z-6217-32011-2036-7@ubuntu-phablet
+ACTION:DISPLAY
+DESCRIPTION:Recurring Alarm
+END:VALARM
+END:VTODO
+END:VCALENDAR
diff --git a/tests/test-eds-ics-nonrepeating-events.cpp b/tests/test-eds-ics-nonrepeating-events.cpp new file mode 100644 index 0000000..a244f1e --- /dev/null +++ b/tests/test-eds-ics-nonrepeating-events.cpp @@ -0,0 +1,93 @@ +/* + * Copyright 2015 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <algorithm> + +#include <datetime/alarm-queue-simple.h> +#include <datetime/clock-mock.h> +#include <datetime/engine-eds.h> +#include <datetime/planner-range.h> + +#include <gtest/gtest.h> + +#include "glib-fixture.h" +#include "print-to.h" +#include "timezone-mock.h" +#include "wakeup-timer-mock.h" + +using namespace unity::indicator::datetime; +using VAlarmFixture = GlibFixture; + +/*** +**** +***/ + +TEST_F(VAlarmFixture, MultipleAppointments) +{ + // start the EDS engine + auto engine = std::make_shared<EdsEngine>(); + + // we need a consistent timezone for the planner and our local DateTimes + constexpr char const * zone_str {"America/Chicago"}; + auto tz = std::make_shared<MockTimezone>(zone_str); + auto gtz = g_time_zone_new(zone_str); + + // make a planner that looks at the first half of 2015 in EDS + auto planner = std::make_shared<SimpleRangePlanner>(engine, tz); + const DateTime range_begin {gtz, 2015,1, 1, 0, 0, 0.0}; + const DateTime range_end {gtz, 2015,6,31,23,59,59.5}; + planner->range().set(std::make_pair(range_begin, range_end)); + + // give EDS a moment to load + if (planner->appointments().get().empty()) { + g_message("waiting a moment for EDS to load..."); + auto on_appointments_changed = [this](const std::vector<Appointment>& appointments){ + g_message("ah, they loaded"); + if (!appointments.empty()) + g_main_loop_quit(loop); + }; + core::ScopedConnection conn(planner->appointments().changed().connect(on_appointments_changed)); + constexpr int max_wait_sec = 10; + wait_msec(max_wait_sec * G_TIME_SPAN_MILLISECOND); + } + + // what we expect to get... + Appointment expected_appt; + expected_appt.uid = "20150520T000726Z-3878-32011-1770-81@ubuntu-phablet"; + expected_appt.color = "#becedd"; + expected_appt.summary = "Alarm"; + std::array<Alarm,1> expected_alarms = { + Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,5,20,20,00,0)}) + }; + + // compare it to what we actually loaded... + const auto appts = planner->appointments().get(); + EXPECT_EQ(expected_alarms.size(), appts.size()); + for (size_t i=0, n=expected_alarms.size(); i<n; i++) { + const auto& appt = appts[i]; + EXPECT_EQ(expected_appt.uid, appt.uid); + EXPECT_EQ(expected_appt.color, appt.color); + EXPECT_EQ(expected_appt.summary, appt.summary); + EXPECT_EQ(1, appt.alarms.size()); + EXPECT_EQ(expected_alarms[i], appt.alarms[0]); + } + + // cleanup + g_time_zone_unref(gtz); +} diff --git a/tests/test-eds-ics-nonrepeating-events.ics b/tests/test-eds-ics-nonrepeating-events.ics new file mode 100644 index 0000000..f41fba3 --- /dev/null +++ b/tests/test-eds-ics-nonrepeating-events.ics @@ -0,0 +1,27 @@ +BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
+VERSION:2.0
+X-EVOLUTION-DATA-REVISION:2015-05-20T22:39:32.685099Z(1)
+BEGIN:VTODO
+UID:20150520T000726Z-3878-32011-1770-81@ubuntu-phablet
+DTSTAMP:20150520T223932Z
+DTSTART:20150520T200000
+SUMMARY:Alarm
+CATEGORIES:x-canonical-alarm
+SEQUENCE:1
+LAST-MODIFIED:20150520T223932Z
+BEGIN:VALARM
+X-EVOLUTION-ALARM-UID:20150520T223932Z-22506-32011-1771-2@ubuntu-phablet
+ACTION:AUDIO
+ATTACH:file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg
+TRIGGER;VALUE=DURATION;RELATED=START:PT0S
+END:VALARM
+BEGIN:VALARM
+X-EVOLUTION-ALARM-UID:20150520T223932Z-22506-32011-1771-3@ubuntu-phablet
+ACTION:DISPLAY
+DESCRIPTION:Alarm
+TRIGGER;VALUE=DURATION;RELATED=START:PT0S
+END:VALARM
+END:VTODO
+END:VCALENDAR
diff --git a/tests/test-eds-ics-repeating-events.cpp b/tests/test-eds-ics-repeating-events.cpp new file mode 100644 index 0000000..526dcc6 --- /dev/null +++ b/tests/test-eds-ics-repeating-events.cpp @@ -0,0 +1,100 @@ +/* + * Copyright 2015 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <algorithm> + +#include <datetime/alarm-queue-simple.h> +#include <datetime/clock-mock.h> +#include <datetime/engine-eds.h> +#include <datetime/planner-range.h> + +#include <gtest/gtest.h> + +#include "glib-fixture.h" +#include "print-to.h" +#include "timezone-mock.h" +#include "wakeup-timer-mock.h" + +using namespace unity::indicator::datetime; +using VAlarmFixture = GlibFixture; + +/*** +**** +***/ + +TEST_F(VAlarmFixture, MultipleAppointments) +{ + // start the EDS engine + auto engine = std::make_shared<EdsEngine>(); + + // we need a consistent timezone for the planner and our local DateTimes + constexpr char const * zone_str {"America/Chicago"}; + auto tz = std::make_shared<MockTimezone>(zone_str); + auto gtz = g_time_zone_new(zone_str); + + // make a planner that looks at the first half of 2015 in EDS + auto planner = std::make_shared<SimpleRangePlanner>(engine, tz); + const DateTime range_begin {gtz, 2015,1, 1, 0, 0, 0.0}; + const DateTime range_end {gtz, 2015,6,31,23,59,59.5}; + planner->range().set(std::make_pair(range_begin, range_end)); + + // give EDS a moment to load + if (planner->appointments().get().empty()) { + g_message("waiting a moment for EDS to load..."); + auto on_appointments_changed = [this](const std::vector<Appointment>& appointments){ + g_message("ah, they loaded"); + if (!appointments.empty()) + g_main_loop_quit(loop); + }; + core::ScopedConnection conn(planner->appointments().changed().connect(on_appointments_changed)); + constexpr int max_wait_sec = 10; + wait_msec(max_wait_sec * G_TIME_SPAN_MILLISECOND); + } + + // what we expect to get... + Appointment expected_appt; + expected_appt.uid = "20150507T211449Z-4262-32011-1418-1@ubuntu-phablet"; + expected_appt.color = "#becedd"; + expected_appt.summary = "Alarm"; + std::array<Alarm,8> expected_alarms = { + Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,5, 8,16,40,0)}), + Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,5,15,16,40,0)}), + Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,5,22,16,40,0)}), + Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,5,29,16,40,0)}), + Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,6, 5,16,40,0)}), + Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,6,12,16,40,0)}), + Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,6,19,16,40,0)}), + Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,6,26,16,40,0)}) + }; + + // compare it to what we actually loaded... + const auto appts = planner->appointments().get(); + EXPECT_EQ(expected_alarms.size(), appts.size()); + for (size_t i=0, n=expected_alarms.size(); i<n; i++) { + const auto& appt = appts[i]; + EXPECT_EQ(expected_appt.uid, appt.uid); + EXPECT_EQ(expected_appt.color, appt.color); + EXPECT_EQ(expected_appt.summary, appt.summary); + EXPECT_EQ(1, appt.alarms.size()); + EXPECT_EQ(expected_alarms[i], appt.alarms[0]); + } + + // cleanup + g_time_zone_unref(gtz); +} diff --git a/tests/test-eds-ics-repeating-events.ics b/tests/test-eds-ics-repeating-events.ics new file mode 100644 index 0000000..efde5f7 --- /dev/null +++ b/tests/test-eds-ics-repeating-events.ics @@ -0,0 +1,28 @@ +BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
+VERSION:2.0
+X-EVOLUTION-DATA-REVISION:2015-05-07T21:14:49.315443Z(0)
+BEGIN:VTODO
+UID:20150507T211449Z-4262-32011-1418-1@ubuntu-phablet
+DTSTAMP:20150508T211449Z
+DTSTART:20150508T164000
+RRULE:FREQ=WEEKLY;BYDAY=FR
+SUMMARY:Alarm
+CATEGORIES:x-canonical-alarm
+CREATED:20150507T211449Z
+LAST-MODIFIED:20150507T211449Z
+BEGIN:VALARM
+X-EVOLUTION-ALARM-UID:20150507T211449Z-4262-32011-1418-2@ubuntu-phablet
+ACTION:AUDIO
+ATTACH:file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg
+TRIGGER;VALUE=DURATION;RELATED=START:PT0S
+END:VALARM
+BEGIN:VALARM
+X-EVOLUTION-ALARM-UID:20150507T211449Z-4262-32011-1418-3@ubuntu-phablet
+ACTION:DISPLAY
+DESCRIPTION:Alarm
+TRIGGER;VALUE=DURATION;RELATED=START:PT0S
+END:VALARM
+END:VTODO
+END:VCALENDAR
diff --git a/tests/test-eds-valarms.cpp b/tests/test-eds-ics-repeating-valarms.cpp index 47f29e7..47f29e7 100644 --- a/tests/test-eds-valarms.cpp +++ b/tests/test-eds-ics-repeating-valarms.cpp diff --git a/tests/test-eds-valarms-config-files/.local/share/evolution/calendar/system/calendar.ics b/tests/test-eds-ics-repeating-valarms.ics index fe526f4..fe526f4 100644 --- a/tests/test-eds-valarms-config-files/.local/share/evolution/calendar/system/calendar.ics +++ b/tests/test-eds-ics-repeating-valarms.ics |