/* * Copyright © 2014 Canonical Ltd. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser 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 warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Authored by: Pete Woods */ #include #include #include #include #include #include using namespace std; namespace lomiri { namespace gmenuharness { namespace { enum class LinkType { any, section, submenu }; static string bool_to_string(bool value) { return value? "true" : "false"; } static shared_ptr get_action_group_attribute(const shared_ptr& actionGroup, const gchar* attribute) { shared_ptr value( g_action_group_get_action_state(actionGroup.get(), attribute), &gvariant_deleter); return value; } static shared_ptr get_attribute(const shared_ptr menuItem, const gchar* attribute) { shared_ptr value( g_menu_item_get_attribute_value(menuItem.get(), attribute, nullptr), &gvariant_deleter); return value; } static string get_string_attribute(const shared_ptr menuItem, const gchar* attribute) { string result; char* temp = nullptr; if (g_menu_item_get_attribute(menuItem.get(), attribute, "s", &temp)) { result = temp; g_free(temp); } return result; } static pair split_action(const string& action) { auto index = action.find('.'); if (index == string::npos) { return make_pair(string(), action); } return make_pair(action.substr(0, index), action.substr(index + 1, action.size())); } static string type_to_string(MenuItemMatcher::Type type) { switch(type) { case MenuItemMatcher::Type::plain: return "plain"; case MenuItemMatcher::Type::checkbox: return "checkbox"; case MenuItemMatcher::Type::radio: return "radio"; } return string(); } } struct MenuItemMatcher::Priv { void all(MatchResult& matchResult, const vector& location, const shared_ptr& menu, map>& actions) { int count = g_menu_model_get_n_items(menu.get()); if (m_items.size() != (unsigned int) count) { matchResult.failure( location, "Expected " + to_string(m_items.size()) + " children, but found " + to_string(count)); return; } for (size_t i = 0; i < m_items.size(); ++i) { const auto& matcher = m_items.at(i); matcher.match(matchResult, location, menu, actions, i); } } void startsWith(MatchResult& matchResult, const vector& location, const shared_ptr& menu, map>& actions) { int count = g_menu_model_get_n_items(menu.get()); if (m_items.size() > (unsigned int) count) { matchResult.failure( location, "Expected at least " + to_string(m_items.size()) + " children, but found " + to_string(count)); return; } for (size_t i = 0; i < m_items.size(); ++i) { const auto& matcher = m_items.at(i); matcher.match(matchResult, location, menu, actions, i); } } void endsWith(MatchResult& matchResult, const vector& location, const shared_ptr& menu, map>& actions) { int count = g_menu_model_get_n_items(menu.get()); if (m_items.size() > (unsigned int) count) { matchResult.failure( location, "Expected at least " + to_string(m_items.size()) + " children, but found " + to_string(count)); return; } // match the last N items size_t j; for (size_t i = count - m_items.size(), j = 0; i < count && j < m_items.size(); ++i, ++j) { const auto& matcher = m_items.at(j); matcher.match(matchResult, location, menu, actions, i); } } Type m_type = Type::plain; Mode m_mode = Mode::all; LinkType m_linkType = LinkType::any; shared_ptr m_expectedSize; shared_ptr m_label; shared_ptr m_icon; map, vector> m_themed_icons; shared_ptr m_action; vector m_state_icons; vector>> m_attributes; vector m_not_exist_attributes; vector>> m_pass_through_attributes; shared_ptr m_isToggled; vector m_items; vector>> m_activations; vector>> m_setActionStates; double m_maxDifference = 0.0; }; MenuItemMatcher MenuItemMatcher::checkbox() { MenuItemMatcher matcher; matcher.type(Type::checkbox); return matcher; } MenuItemMatcher MenuItemMatcher::radio() { MenuItemMatcher matcher; matcher.type(Type::radio); return matcher; } MenuItemMatcher::MenuItemMatcher() : p(new Priv) { } MenuItemMatcher::~MenuItemMatcher() { } MenuItemMatcher::MenuItemMatcher(const MenuItemMatcher& other) : p(new Priv) { *this = other; } MenuItemMatcher::MenuItemMatcher(MenuItemMatcher&& other) { *this = move(other); } MenuItemMatcher& MenuItemMatcher::operator=(const MenuItemMatcher& other) { p->m_type = other.p->m_type; p->m_mode = other.p->m_mode; p->m_expectedSize = other.p->m_expectedSize; p->m_label = other.p->m_label; p->m_icon = other.p->m_icon; p->m_themed_icons = other.p->m_themed_icons; p->m_action = other.p->m_action; p->m_state_icons = other.p->m_state_icons; p->m_attributes = other.p->m_attributes; p->m_not_exist_attributes = other.p->m_not_exist_attributes; p->m_pass_through_attributes = other.p->m_pass_through_attributes; p->m_isToggled = other.p->m_isToggled; p->m_linkType = other.p->m_linkType; p->m_items = other.p->m_items; p->m_activations = other.p->m_activations; p->m_setActionStates = other.p->m_setActionStates; p->m_maxDifference = other.p->m_maxDifference; return *this; } MenuItemMatcher& MenuItemMatcher::operator=(MenuItemMatcher&& other) { p = move(other.p); return *this; } MenuItemMatcher& MenuItemMatcher::type(Type type) { p->m_type = type; return *this; } MenuItemMatcher& MenuItemMatcher::label(const string& label) { p->m_label = make_shared(label); return *this; } MenuItemMatcher& MenuItemMatcher::action(const string& action) { p->m_action = make_shared(action); return *this; } MenuItemMatcher& MenuItemMatcher::state_icons(const std::vector& state_icons) { p->m_state_icons = state_icons; return *this; } MenuItemMatcher& MenuItemMatcher::icon(const string& icon) { p->m_icon = make_shared(icon); return *this; } MenuItemMatcher& MenuItemMatcher::themed_icon(const std::string& iconName, const std::vector& icons) { p->m_themed_icons[make_shared(iconName)] = icons; return *this; } MenuItemMatcher& MenuItemMatcher::widget(const string& widget) { return string_attribute("x-ayatana-type", widget); } MenuItemMatcher& MenuItemMatcher::pass_through_attribute(const string& actionName, const shared_ptr& value) { p->m_pass_through_attributes.emplace_back(actionName, value); return *this; } MenuItemMatcher& MenuItemMatcher::pass_through_boolean_attribute(const string& actionName, bool value) { return pass_through_attribute( actionName, shared_ptr(g_variant_new_boolean(value), &gvariant_deleter)); } MenuItemMatcher& MenuItemMatcher::pass_through_string_attribute(const string& actionName, const string& value) { return pass_through_attribute( actionName, shared_ptr(g_variant_new_string(value.c_str()), &gvariant_deleter)); } MenuItemMatcher& MenuItemMatcher::pass_through_double_attribute(const std::string& actionName, double value) { return pass_through_attribute( actionName, shared_ptr(g_variant_new_double(value), &gvariant_deleter)); } MenuItemMatcher& MenuItemMatcher::round_doubles(double maxDifference) { p->m_maxDifference = maxDifference; return *this; } MenuItemMatcher& MenuItemMatcher::attribute(const string& name, const shared_ptr& value) { p->m_attributes.emplace_back(name, value); return *this; } MenuItemMatcher& MenuItemMatcher::boolean_attribute(const string& name, bool value) { return attribute( name, shared_ptr(g_variant_new_boolean(value), &gvariant_deleter)); } MenuItemMatcher& MenuItemMatcher::string_attribute(const string& name, const string& value) { return attribute( name, shared_ptr(g_variant_new_string(value.c_str()), &gvariant_deleter)); } MenuItemMatcher& MenuItemMatcher::int32_attribute(const std::string& name, int value) { return attribute( name, shared_ptr(g_variant_new_int32 (value), &gvariant_deleter)); } MenuItemMatcher& MenuItemMatcher::int64_attribute(const std::string& name, int value) { return attribute( name, shared_ptr(g_variant_new_int64 (value), &gvariant_deleter)); } MenuItemMatcher& MenuItemMatcher::double_attribute(const std::string& name, double value) { return attribute( name, shared_ptr(g_variant_new_double (value), &gvariant_deleter)); } MenuItemMatcher& MenuItemMatcher::attribute_not_set(const std::string& name) { p->m_not_exist_attributes.emplace_back (name); return *this; } MenuItemMatcher& MenuItemMatcher::toggled(bool isToggled) { p->m_isToggled = make_shared(isToggled); return *this; } MenuItemMatcher& MenuItemMatcher::mode(Mode mode) { p->m_mode = mode; return *this; } MenuItemMatcher& MenuItemMatcher::submenu() { p->m_linkType = LinkType::submenu; return *this; } MenuItemMatcher& MenuItemMatcher::section() { p->m_linkType = LinkType::section; return *this; } MenuItemMatcher& MenuItemMatcher::is_empty() { return has_exactly(0); } MenuItemMatcher& MenuItemMatcher::has_exactly(size_t children) { p->m_expectedSize = make_shared(children); return *this; } MenuItemMatcher& MenuItemMatcher::item(const MenuItemMatcher& item) { p->m_items.emplace_back(item); return *this; } MenuItemMatcher& MenuItemMatcher::item(MenuItemMatcher&& item) { p->m_items.emplace_back(item); return *this; } MenuItemMatcher& MenuItemMatcher::pass_through_activate(std::string const& action, const shared_ptr& parameter) { p->m_activations.emplace_back(action, parameter); return *this; } MenuItemMatcher& MenuItemMatcher::activate(const shared_ptr& parameter) { p->m_activations.emplace_back(string(), parameter); return *this; } MenuItemMatcher& MenuItemMatcher::set_pass_through_action_state(const std::string& action, const std::shared_ptr& state) { p->m_setActionStates.emplace_back(action, state); return *this; } MenuItemMatcher& MenuItemMatcher::set_action_state(const std::shared_ptr& state) { p->m_setActionStates.emplace_back("", state); return *this; } void MenuItemMatcher::match( MatchResult& matchResult, const vector& parentLocation, const shared_ptr& menu, map>& actions, int index) const { shared_ptr menuItem(g_menu_item_new_from_model(menu.get(), index), &g_object_deleter); vector location(parentLocation); location.emplace_back(index); string action = get_string_attribute(menuItem, G_MENU_ATTRIBUTE_ACTION); bool isCheckbox = false; bool isRadio = false; bool isToggled = false; pair idPair; shared_ptr actionGroup; shared_ptr state; if (!action.empty()) { idPair = split_action(action); actionGroup = actions[idPair.first]; state = shared_ptr(g_action_group_get_action_state(actionGroup.get(), idPair.second.c_str()), &gvariant_deleter); auto attributeTarget = get_attribute(menuItem, G_MENU_ATTRIBUTE_TARGET); if (attributeTarget && state) { isToggled = g_variant_equal(state.get(), attributeTarget.get()); isRadio = true; } else if (state && g_variant_is_of_type(state.get(), G_VARIANT_TYPE_BOOLEAN)) { isToggled = g_variant_get_boolean(state.get()); isCheckbox = true; } } Type actualType = Type::plain; if (isCheckbox) { actualType = Type::checkbox; } else if (isRadio) { actualType = Type::radio; } if (actualType != p->m_type) { matchResult.failure( location, "Expected " + type_to_string(p->m_type) + ", found " + type_to_string(actualType)); } // check themed icons map, vector>::iterator iter; for (iter = p->m_themed_icons.begin(); iter != p->m_themed_icons.end(); ++iter) { auto icon_val = g_menu_item_get_attribute_value(menuItem.get(), (*iter).first->c_str(), nullptr); if (!icon_val) { matchResult.failure( location, "Expected themed icon " + (*(*iter).first) + " was not found"); } auto gicon = g_icon_deserialize(icon_val); if (!gicon || !G_IS_THEMED_ICON(gicon)) { matchResult.failure( location, "Expected attribute " + (*(*iter).first) + " is not a themed icon"); } auto iconNames = g_themed_icon_get_names(G_THEMED_ICON(gicon)); int nb_icons = 0; while(iconNames[nb_icons]) { ++nb_icons; } if (nb_icons != (*iter).second.size()) { matchResult.failure( location, "Expected " + to_string((*iter).second.size()) + " icons for themed icon [" + (*(*iter).first) + "], but " + to_string(nb_icons) + " were found."); } else { // now compare all the icons for (int i = 0; i < nb_icons; ++i) { if (string(iconNames[i]) != (*iter).second[i]) { matchResult.failure( location, "Icon at position " + to_string(i) + " for themed icon [" + (*(*iter).first) + "], mismatchs. Expected: " + iconNames[i] + " but found " + (*iter).second[i]); } } } g_object_unref(gicon); } string label = get_string_attribute(menuItem, G_MENU_ATTRIBUTE_LABEL); if (p->m_label && (*p->m_label) != label) { matchResult.failure( location, "Expected label '" + *p->m_label + "', but found '" + label + "'"); } string icon = get_string_attribute(menuItem, G_MENU_ATTRIBUTE_ICON); if (p->m_icon && (*p->m_icon) != icon) { matchResult.failure( location, "Expected icon '" + *p->m_icon + "', but found '" + icon + "'"); } if (p->m_action && (*p->m_action) != action) { matchResult.failure( location, "Expected action '" + *p->m_action + "', but found '" + action + "'"); } if (!p->m_state_icons.empty() && !state) { matchResult.failure( location, "Expected state icons but no state was found"); } else if (!p->m_state_icons.empty() && state && !g_variant_is_of_type(state.get(), G_VARIANT_TYPE_VARDICT)) { matchResult.failure( location, "Expected state icons vardict, found " + type_to_string(actualType)); } else if (!p->m_state_icons.empty() && state && g_variant_is_of_type(state.get(), G_VARIANT_TYPE_VARDICT)) { std::vector actual_state_icons; GVariantIter it; gchar* key; GVariant* value; g_variant_iter_init(&it, state.get()); while (g_variant_iter_loop(&it, "{sv}", &key, &value)) { if (std::string(key) == "icon") { auto gicon = g_icon_deserialize(value); if (gicon && G_IS_THEMED_ICON(gicon)) { auto iconNames = g_themed_icon_get_names(G_THEMED_ICON(gicon)); // Just take the first icon in the list (there is only ever one) actual_state_icons.push_back(iconNames[0]); g_object_unref(gicon); } } else if (std::string(key) == "icons" && g_variant_is_of_type(value, G_VARIANT_TYPE("av"))) { // If we find "icons" in the map, clear any icons we may have found in "icon", // then break from the loop as we have found all icons now. actual_state_icons.clear(); GVariantIter icon_it; GVariant* icon_value; g_variant_iter_init(&icon_it, value); while (g_variant_iter_loop(&icon_it, "v", &icon_value)) { auto gicon = g_icon_deserialize(icon_value); if (gicon && G_IS_THEMED_ICON(gicon)) { auto iconNames = g_themed_icon_get_names(G_THEMED_ICON(gicon)); // Just take the first icon in the list (there is only ever one) actual_state_icons.push_back(iconNames[0]); g_object_unref(gicon); } } // We're breaking out of g_variant_iter_loop here so clean up g_variant_unref(value); g_free(key); break; } } if (p->m_state_icons != actual_state_icons) { std::string expected_icons; for (unsigned int i = 0; i < p->m_state_icons.size(); ++i) { expected_icons += i == 0 ? p->m_state_icons[i] : ", " + p->m_state_icons[i]; } std::string actual_icons; for (unsigned int i = 0; i < actual_state_icons.size(); ++i) { actual_icons += i == 0 ? actual_state_icons[i] : ", " + actual_state_icons[i]; } matchResult.failure( location, "Expected state_icons == {" + expected_icons + "} but found {" + actual_icons + "}"); } } for (const auto& e: p->m_pass_through_attributes) { string actionName = get_string_attribute(menuItem, e.first.c_str()); if (actionName.empty()) { matchResult.failure( location, "Could not find action name '" + e.first + "'"); } else { auto passThroughIdPair = split_action(actionName); auto actionGroup = actions[passThroughIdPair.first]; if (actionGroup) { auto value = get_action_group_attribute( actionGroup, passThroughIdPair.second.c_str()); if (!value) { matchResult.failure( location, "Expected pass-through attribute '" + e.first + "' was not present"); } else if (!g_variant_is_of_type(e.second.get(), g_variant_get_type(value.get()))) { std::string expectedType = g_variant_get_type_string(e.second.get()); std::string actualType = g_variant_get_type_string(value.get()); matchResult.failure( location, "Expected pass-through attribute type '" + expectedType + "' but found '" + actualType + "'"); } else if (g_variant_compare(e.second.get(), value.get())) { bool reportMismatch = true; if (g_strcmp0(g_variant_get_type_string(value.get()),"d") == 0 && p->m_maxDifference) { auto actualDouble = g_variant_get_double(value.get()); auto expectedDouble = g_variant_get_double(e.second.get()); auto difference = actualDouble-expectedDouble; if (difference < 0) difference = difference * -1.0; if (difference <= p->m_maxDifference) { reportMismatch = false; } } if (reportMismatch) { gchar* expectedString = g_variant_print(e.second.get(), true); gchar* actualString = g_variant_print(value.get(), true); matchResult.failure( location, "Expected pass-through attribute '" + e.first + "' == " + expectedString + " but found " + actualString); g_free(expectedString); g_free(actualString); } } } else { matchResult.failure(location, "Could not find action group for ID '" + passThroughIdPair.first + "'"); } } } for (const auto& e: p->m_attributes) { auto value = get_attribute(menuItem, e.first.c_str()); if (!value) { matchResult.failure(location, "Expected attribute '" + e.first + "' could not be found"); } else if (!g_variant_is_of_type(e.second.get(), g_variant_get_type(value.get()))) { std::string expectedType = g_variant_get_type_string(e.second.get()); std::string actualType = g_variant_get_type_string(value.get()); matchResult.failure( location, "Expected attribute type '" + expectedType + "' but found '" + actualType + "'"); } else if (g_variant_compare(e.second.get(), value.get())) { gchar* expectedString = g_variant_print(e.second.get(), true); gchar* actualString = g_variant_print(value.get(), true); matchResult.failure( location, "Expected attribute '" + e.first + "' == " + expectedString + ", but found " + actualString); g_free(expectedString); g_free(actualString); } } for (const auto& e: p->m_not_exist_attributes) { auto value = get_attribute(menuItem, e.c_str()); if (value) { matchResult.failure(location, "Not expected attribute '" + e + "' was found"); } } if (p->m_isToggled && (*p->m_isToggled) != isToggled) { matchResult.failure( location, "Expected toggled = " + bool_to_string(*p->m_isToggled) + ", but found " + bool_to_string(isToggled)); } if (!matchResult.success()) { return; } if (!p->m_items.empty() || p->m_expectedSize) { shared_ptr link; switch (p->m_linkType) { case LinkType::any: { link.reset(g_menu_model_get_item_link(menu.get(), (int) index, G_MENU_LINK_SUBMENU), &g_object_deleter); if (!link) { link.reset(g_menu_model_get_item_link(menu.get(), (int) index, G_MENU_LINK_SECTION), &g_object_deleter); } break; } case LinkType::submenu: { link.reset(g_menu_model_get_item_link(menu.get(), (int) index, G_MENU_LINK_SUBMENU), &g_object_deleter); break; } case LinkType::section: { link.reset(g_menu_model_get_item_link(menu.get(), (int) index, G_MENU_LINK_SECTION), &g_object_deleter); break; } } if (!link) { if (p->m_expectedSize) { matchResult.failure( location, "Expected " + to_string(*p->m_expectedSize) + " children, but found none"); } else { matchResult.failure( location, "Expected " + to_string(p->m_items.size()) + " children, but found none"); } return; } else { while (true) { MatchResult childMatchResult(matchResult.createChild()); if (p->m_expectedSize && *p->m_expectedSize != (unsigned int) g_menu_model_get_n_items( link.get())) { childMatchResult.failure( location, "Expected " + to_string(*p->m_expectedSize) + " child items, but found " + to_string( g_menu_model_get_n_items( link.get()))); } else if (!p->m_items.empty()) { switch (p->m_mode) { case Mode::all: p->all(childMatchResult, location, link, actions); break; case Mode::starts_with: p->startsWith(childMatchResult, location, link, actions); break; case Mode::ends_with: p->endsWith(childMatchResult, location, link, actions); break; } } if (childMatchResult.success()) { matchResult.merge(childMatchResult); break; } else { if (matchResult.hasTimedOut()) { matchResult.merge(childMatchResult); break; } menuWaitForItems(link); } } } } for (const auto& a: p->m_setActionStates) { auto stateAction = action; auto stateIdPair = idPair; auto stateActionGroup = actionGroup; if (!a.first.empty()) { stateAction = get_string_attribute(menuItem, a.first.c_str());; stateIdPair = split_action(stateAction); stateActionGroup = actions[stateIdPair.first]; } if (stateAction.empty()) { matchResult.failure( location, "Tried to set action state, but no action was found"); } else if(!stateActionGroup) { matchResult.failure( location, "Tried to set action state for action group '" + stateIdPair.first + "', but action group wasn't found"); } else if (!g_action_group_has_action(stateActionGroup.get(), stateIdPair.second.c_str())) { matchResult.failure( location, "Tried to set action state for action '" + stateAction + "', but action was not found"); } else { g_action_group_change_action_state(stateActionGroup.get(), stateIdPair.second.c_str(), g_variant_ref(a.second.get())); } // FIXME this is a dodgy way to ensure the action state change gets dispatched menuWaitForItems(menu, 100); } for (const auto& a: p->m_activations) { string tmpAction = action; auto tmpIdPair = idPair; auto tmpActionGroup = actionGroup; if (!a.first.empty()) { tmpAction = get_string_attribute(menuItem, a.first.c_str()); tmpIdPair = split_action(tmpAction); tmpActionGroup = actions[tmpIdPair.first]; } if (tmpAction.empty()) { matchResult.failure( location, "Tried to activate action, but no action was found"); } else if(!tmpActionGroup) { matchResult.failure( location, "Tried to activate action group '" + tmpIdPair.first + "', but action group wasn't found"); } else if (!g_action_group_has_action(tmpActionGroup.get(), tmpIdPair.second.c_str())) { matchResult.failure( location, "Tried to activate action '" + tmpAction + "', but action was not found"); } else { if (a.second) { g_action_group_activate_action(tmpActionGroup.get(), tmpIdPair.second.c_str(), g_variant_ref(a.second.get())); } else { g_action_group_activate_action(tmpActionGroup.get(), tmpIdPair.second.c_str(), nullptr); } // FIXME this is a dodgy way to ensure the activation gets dispatched menuWaitForItems(menu, 100); } } } } // namepsace gmenuharness } // namespace lomiri