#include <boost/shared_ptr.hpp>
#include <cstdint>
+#include <set>
#include <string>
namespace isc {
namespace cb {
+/// @brief Server selector for associating objects in a database with
+/// specific servers.
+///
+/// Configuration information stored in the configuration backends can be
+/// associated with selected servers, all servers or no particular server.
+/// For example: a particular subnet definition in the database may be
+/// associated with one server or can be shared by multiple servers.
+/// In the latter case, a subnet may be associated with a subset of
+/// servers or all servers. An administrator may also add the
+/// configuration data into the database and do not associate this data
+/// with any patrticular server.
+///
+/// When fetching the configuration data from a databse or when storing
+/// data in the database there is a need to specify which servers this
+/// data is associated with. The @c ServerSelector class represents
+/// such associations.
+///
+/// It includes three modes of selection: UNASSIGNED, ALL and SUBSET and
+/// several factory functions making associations described above.
+///
+/// The @c ServerSelector class should be used in objects derived from
+/// @c BaseConfigBackendPool and in objects derived from
+/// @c BaseConfigBackend to indicate which servers the specific calls
+/// exposed by these objects refer to.
+class ServerSelector {
+public:
+
+ /// @brief Type of the server selection.
+ enum class Type {
+ UNASSIGNED,
+ ALL,
+ SUBSET
+ };
+
+ /// @brief Factory returning "unassigned" server selector.
+ static ServerSelector& UNASSIGNED() {
+ static ServerSelector selector(Type::UNASSIGNED);
+ return (selector);
+ }
+
+ /// @brief Factory returning "all servers" selector.
+ static ServerSelector& ALL() {
+ static ServerSelector selector(Type::ALL);
+ return (selector);
+ }
+
+ /// @brief Factory returning selector of one server.
+ ///
+ /// @param server_tag tag of the single server to be selected.
+ static ServerSelector& ONE(const std::string& server_tag) {
+ static ServerSelector selector(server_tag);
+ return (selector);
+ }
+
+ /// @brief Factory returning "multiple servers" selector.
+ ///
+ /// @param server_tags set of server tags to be selected.
+ static ServerSelector& MULTIPLE(const std::set<std::string>& server_tags) {
+ static ServerSelector selector(server_tags);
+ return (selector);
+ }
+
+ /// @brief Returns type of the selector.
+ Type getType() const {
+ return (type_);
+ }
+
+ /// @brief Returns tags associated with the selector.
+ ///
+ /// @return server tags for mutliple selections and for one server,
+ /// empty set for all servers and and unassigned.
+ std::set<std::string> getTags() const {
+ return (tags_);
+ }
+
+private:
+
+ /// @brief Constructor used for "unassigned" and "all" slection types.
+ ///
+ /// @param type selector type.
+ explicit ServerSelector(const Type& type)
+ : type_(type), tags_() {
+ }
+
+ /// @brief Constructor used for selecting a single server.
+ ///
+ /// @param server_tag tag of the server to be selected.
+ explicit ServerSelector(const std::string& server_tag)
+ : type_(Type::SUBSET), tags_() {
+ tags_.insert(server_tag);
+ }
+
+ /// @brief Constructor used for selecting multiple servers.
+ ///
+ /// @param server_tags set of server tags.
+ explicit ServerSelector(const std::set<std::string>& server_tags)
+ : type_(Type::SUBSET), tags_(server_tags) {
+ }
+
+ /// @brief Selection type used.
+ Type type_;
+
+ /// @brief Holds tags of explicitly selected servers.
+ std::set<std::string> tags_;
+};
+
/// @brief Base class for server specific configuration backends.
class BaseConfigBackend {
public:
/// type will expose a different API calls.
///
/// This template class is a base class for all pools used by various servers.
-/// It implements mechanisms for managing multiple backends and for routing
+/// It implements mechanisms for managing multiple backends and for forwarding
/// API calls to one or many database backends depending on the selections
/// made via @c BackendSelector class.
///
/// @brief Retrieve a single configuration property from the pool.
///
- /// This is a common method for retrieving a single configuration property
+ /// This is common method for retrieving a single configuration property
/// from the databases. The server specific backends call this method to
/// retrieve a single object. For example, the DHCPv4 configuration backend
/// pool may use this function to implement a @c getSubnet4 method:
///
/// @code
/// Subnet4Ptr getSubnet4(const SubnetID& subnet_id,
- /// const BackendSelector& selector) const {
+ /// const BackendSelector& selector,
+ /// const ServerSelector& server_selector) const {
/// Subnet4Ptr subnet;
/// getPropertyPtrConst<Subnet4Ptr, const SubnetID&, ConfigBackendDHCPv4::getSubnet4>
- /// (subnet, subnet_id, selector);
+ /// (&ConfigBackendDHCPv4::getSubnet4, selector, subnet, subnet_id);
/// return (subnet);
/// }
/// @endcode
/// where @c ConfigBackendDHCPv4::getSubnet4 has the following signature:
///
/// @code
- /// Subnet4Ptr getSubnet4(const SubnetID& subnet_id) const;
+ /// Subnet4Ptr getSubnet4(const ServerSelector&, const SubnetID&) const;
/// @endcode
///
/// If the backend selector is set to "unspecified", this method will iterate
/// rest of the backends are skipped.
///
/// @tparam PropertyType Type of the object returned by the backend call.
- /// @tparam InputType Type of the object used as input to the backend call.
- /// @tparam MethodPointer Type of the pointer to the backend method to be
- /// called.
+ /// @tparam InputType Type of the objects used as input to the backend call.
///
+ /// @param MethodPointer Pointer to the backend method to be called.
+ /// @param selector Backend selector.
+ /// @param server_selector Server selector.
/// @param [out] property Reference to the shared pointer where retrieved
/// property should be assigned.
- /// @param input Value to be used as input to the backend call.
- /// @param selector Backend selector. By default it is unspecified.
+ /// @param input Values to be used as input to the backend call.
///
/// @throw db::NoSuchDatabase if no database matching the given selector
/// was found.
- template<typename PropertyType, typename InputType,
- PropertyType (ConfigBackendType::*MethodPointer)(InputType) const>
- void getPropertyPtrConst(PropertyType& property, InputType input,
- const BackendSelector& selector =
- BackendSelector::UNSPEC()) const {
+ template<typename PropertyType, typename... InputType>
+ void getPropertyPtrConst(PropertyType (ConfigBackendType::*MethodPointer)
+ (const ServerSelector&, InputType...) const,
+ const BackendSelector& selector,
+ const ServerSelector& server_selector,
+ PropertyType& property,
+ InputType... input) const {
// If no particular backend is selected, call each backend and return
// the first non-null (non zero) value.
if (selector.amUnspecified()) {
for (auto backend : backends_) {
- property = ((*backend).*MethodPointer)(input);
+ property = ((*backend).*MethodPointer)(server_selector, input...);
if (property) {
break;
}
auto backends = selectBackends(selector);
if (!backends.empty()) {
for (auto backend : backends) {
- property = ((*backend).*MethodPointer)(input);
+ property = ((*backend).*MethodPointer)(server_selector, input...);
if (property) {
break;
}
/// @c getSubnets6 method:
///
/// @code
- /// Subnet6Collection getModifiedSubnets6(const ptime& modification_time,
- /// const BackendSelector& selector) const {
+ /// Subnet6Collection getModifiedSubnets6(const BackendSelector& selector,
+ /// const ServerSelector& server_selector,
+ /// const ptime& modification_time) const {
/// Subnet6Collection subnets;
- /// getMultiplePropertiesConst<Subnet6Collection, const ptime&,
- /// ConfigBackendDHCPv6::getSubnets6>
- /// (subnets, modification_time, selector);
- /// return (subnets);
+ /// getMultiplePropertiesConst<Subnet6Collection, const ptime&>
+ /// (&ConfigBackendDHCPv6::getSubnets6, selector, subnets,
+ /// modification_time);
+ /// return (subnets);
/// }
/// @endcode
///
/// where @c ConfigBackendDHCPv6::getSubnets6 has the following signature:
///
/// @code
- /// Subnet6Collection getSubnets6(const ptime& modification_time) const;
+ /// Subnet6Collection getSubnets6(const ServerSelector&, const ptime&) const;
/// @endcode
///
/// If the backend selector is set to "unspecified", this method will iterate
///
/// @tparam PropertyCollectionType Type of the container into which the
/// properties are stored.
- /// @tparam InputType type of the object used as input to the backend call.
- /// @tparam MethodPointer Type of the pointer to the backend method to be
- /// called.
+ /// @tparam InputType Type of the objects used as input to the backend call.
///
+ /// @param MethodPointer Pointer to the backend method to be called.
+ /// @param selector Backend selector.
+ /// @param server_selector Server selector.
/// @param [out] properties Reference to the collection of retrieved properties.
- /// @param inputValue to be used as input to the backend call.
- /// @param selector Backend selector. By default it is unspecified.
+ /// @param input Values to be used as input to the backend call.
///
/// @throw db::NoSuchDatabase if no database matching the given selector
/// was found.
- template<typename PropertyCollectionType, typename InputType,
- PropertyCollectionType (ConfigBackendType::*MethodPointer)(InputType) const>
- void getMultiplePropertiesConst(PropertyCollectionType& properties,
- InputType input,
- const BackendSelector& selector =
- BackendSelector::UNSPEC()) const {
+ template<typename PropertyCollectionType, typename... InputType>
+ void getMultiplePropertiesConst(PropertyCollectionType (ConfigBackendType::*MethodPointer)
+ (const ServerSelector&, InputType...) const,
+ const BackendSelector& selector,
+ const ServerSelector& server_selector,
+ PropertyCollectionType& properties,
+ InputType... input) const {
if (selector.amUnspecified()) {
for (auto backend : backends_) {
- properties = ((*backend).*MethodPointer)(input);
+ properties = ((*backend).*MethodPointer)(server_selector, input...);
if (!properties.empty()) {
break;
}
auto backends = selectBackends(selector);
if (!backends.empty()) {
for (auto backend : backends) {
- properties = ((*backend).*MethodPointer)(input);
+ properties = ((*backend).*MethodPointer)(server_selector, input...);
if (!properties.empty()) {
break;
}
/// @c getAllSubnets4 method:
///
/// @code
- /// Subnet4Collection getAllSubnets4(const BackendSelector& selector) const {
+ /// Subnet4Collection getAllSubnets4(const BackendSelector&, const ServerSelector&) const {
/// Subnet4Collection subnets;
- /// getAllPropertiesConst<Subnet6Collection, ConfigBackendDHCPv4::getAllSubnets4>
- /// (subnets, selector);
+ /// getAllPropertiesConst<Subnet6Collection>
+ /// (&ConfigBackendDHCPv4::getAllSubnets4, subnets, selector,
+ /// server_selector);
/// return (subnets);
/// }
/// @endcode
/// where @c ConfigBackendDHCPv4::getAllSubnets4 has the following signature:
///
/// @code
- /// Subnet4Collection getAllSubnets4() const;
+ /// Subnet4Collection getAllSubnets4(const ServerSelector&) const;
/// @endcode
///
/// If the backend selector is set to "unspecified", this method will iterate
///
/// @tparam PropertyCollectionType Type of the container into which the
/// properties are stored.
- /// @tparam MethodPointer Type of the pointer to the backend method to be
- /// called.
///
+ /// @param MethodPointer Pointer to the backend method to be called.
+ /// @param selector Backend selector.
+ /// @param server_selector Server selector.
/// @param [out] properties Reference to the collection of retrieved properties.
- /// @param selector Backend selector. By default it is unspecified.
///
/// @throw db::NoSuchDatabase if no database matching the given selector
/// was found.
- template<typename PropertyCollectionType,
- PropertyCollectionType (ConfigBackendType::*MethodPointer)() const>
- void getAllPropertiesConst(PropertyCollectionType& properties,
- const BackendSelector& selector =
- BackendSelector::UNSPEC()) const {
+ template<typename PropertyCollectionType>
+ void getAllPropertiesConst(PropertyCollectionType (ConfigBackendType::*MethodPointer)
+ (const ServerSelector&) const,
+ const BackendSelector& selector,
+ const ServerSelector& server_selector,
+ PropertyCollectionType& properties) const {
if (selector.amUnspecified()) {
for (auto backend : backends_) {
- properties = ((*backend).*MethodPointer)();
+ properties = ((*backend).*MethodPointer)(server_selector);
if (!properties.empty()) {
break;
}
auto backends = selectBackends(selector);
if (!backends.empty()) {
for (auto backend : backends) {
- properties = ((*backend).*MethodPointer)();
+ properties = ((*backend).*MethodPointer)(server_selector);
if (!properties.empty()) {
break;
}
///
/// @code
/// void createUpdateSubnet6(const Subnet6Ptr& subnet,
- /// const BackendSelector& selector) {
- /// createUpdateDeleteProperty<const Subnet6Ptr&,
- /// ConfigBackendDHCPv6::createUpdateSubnet6>
- /// (subnet, selector);
+ /// const BackendSelector& selector,
+ /// const ServerSelector& server_selector) {
+ /// createUpdateDeleteProperty<const Subnet6Ptr&>
+ /// (&ConfigBackendDHCPv6::createUpdateSubnet6, selector,
+ /// server_selector, subnet, selector);
/// }
/// @endcode
///
/// signature:
///
/// @code
- /// void createUpdateSubnet6(const Subnet6Ptr& subnet);
+ /// void createUpdateSubnet6(const ServerSelector&, const Subnet6Ptr&);
/// @endcode
///
/// The backend selector must point to exactly one backend. If more than one
/// backend is selected, an exception is thrown. If no backend is selected
/// an exception is thrown either.
///
- /// @tparam InputType Type of the object being a new property to be added
- /// or updated, or an identifier of the object to be deleted.
- /// @tparam MethodPointer Type of the pointer to the backend method to be
- /// called.
+ /// @tparam InputType Type of the objects being used as arguments of the
+ /// backend method, e.g. new property to be added, updated or deleted.
///
- /// @param input Object being a new property to be added or updated, or an
- /// identifier of the object to be deleted.
+ /// @param MethodPointer Pointer to the backend method to be called.
/// @param selector Backend selector.
+ /// @param server_selector Server selector.
+ /// @param input Objects used as arguments to the backend method to be
+ /// called.
///
/// @throw db::NoSuchDatabase if no database matching the given selector
/// was found.
/// @throw db::AmbiguousDatabase if multiple databases matching the selector
/// were found.
- template<typename InputType,
- void (ConfigBackendType::*MethodPointer)(InputType)>
- void createUpdateDeleteProperty(InputType input, const BackendSelector& selector) {
+ template<typename... InputType>
+ void createUpdateDeleteProperty(void (ConfigBackendType::*MethodPointer)
+ (const ServerSelector&, InputType...),
+ const BackendSelector& selector,
+ const ServerSelector& server_selector,
+ InputType... input) {
auto backends = selectBackends(selector);
if (backends.empty()) {
isc_throw(db::NoSuchDatabase, "no database found for selector: "
"selector: " << selector.toText());
}
- (*(*(backends.begin())).*MethodPointer)(input);
+ (*(*(backends.begin())).*MethodPointer)(server_selector, input...);
}
/// @brief Selects existing backends matching the selector.
libcb_unittests_SOURCES = config_backend_mgr_unittest.cc
libcb_unittests_SOURCES += config_backend_selector_unittest.cc
libcb_unittests_SOURCES += run_unittests.cc
+libcb_unittests_SOURCES += server_selector_unittest.cc
libcb_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
libcb_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
///
/// @param property_name Name of the property to be retrieved.
/// @return Value of the property or 0 if property doesn't exist.
- virtual int getProperty(const std::string& property_name) const {
+ virtual int getProperty(const ServerSelector&,
+ const std::string& property_name) const {
for (auto property : properties_) {
if (property.first == property_name) {
return (property.second);
return (0);
}
+ /// @brief Retrieves first property matching the name and value.
+ ///
+ /// @param property_name Name of the property to be retrieved.
+ /// @param property_value Value of the property to be retrieved.
+ /// @return Value of the property or 0 if the property doesn't exist.
+ virtual int getProperty(const ServerSelector&,
+ const std::string& property_name,
+ const int property_value) const {
+ for (auto property : properties_) {
+ if ((property.first == property_name) &&
+ (property.second == property_value)) {
+ return (property.second);
+ }
+ }
+ return (0);
+ }
+
/// @brief Retrieves all properties having a given name.
///
/// @param property_name Name of the properties to be retrieved.
/// @return List of the properties having a given name. This list is
/// empty if no property was found.
- virtual PropertiesList getProperties(const std::string& property_name) const {
+ virtual PropertiesList getProperties(const ServerSelector&,
+ const std::string& property_name) const {
PropertiesList properties;
for (auto property : properties_) {
if (property.first == property_name) {
/// @brief Retrieves all properties.
///
/// @return List of all properties held in the backend.
- virtual PropertiesList getAllProperties() const {
+ virtual PropertiesList getAllProperties(const ServerSelector&) const {
return (properties_);
}
/// @brief Creates new property.
///
/// @param new_property Property to be added to the backend.
- virtual void createProperty(const std::pair<std::string, int>& new_property) {
+ virtual void createProperty(const ServerSelector&,
+ const std::pair<std::string, int>& new_property) {
properties_.push_back(new_property);
}
/// @param selector Backend selector. The default value of the selector
/// is @c UNSPEC which means that the property will be searched in all backends
/// and the first value found will be returned.
+ /// @param server_selector Server selector. The default value is set to @c ALL,
+ /// which means that the property for all servers will be returned.
virtual int getProperty(const std::string& property_name,
- const BackendSelector& selector = BackendSelector::UNSPEC()) const {
+ const BackendSelector& selector =
+ BackendSelector::UNSPEC(),
+ const ServerSelector& server_selector =
+ ServerSelector::ALL()) const {
int property;
// If the selector is specified, this method will pick the appropriate
// the value held in the second backend (if any) won't be fetched.
// The template arguments specify the returned value type and the
// argument of the getProperty method.
- getPropertyPtrConst<int,
- const std::string&,
- &TestConfigBackend::getProperty>
- (property, property_name, selector);
+ getPropertyPtrConst<int, const std::string&>
+ (&TestConfigBackend::getProperty, selector, server_selector, property,
+ property_name);
return (property);
}
+ /// @brief Retrieves value of the property.
+ ///
+ /// @param property_name Name of the property which value should be returned.
+ /// @param property_value Value of the property to be retrieved.
+ /// @param selector Backend selector. The default value of the selector
+ /// is @c UNSPEC which means that the property will be searched in all backends
+ /// and the first value found will be returned.
+ /// @param server_selector Server selector. The default value is set to @c ALL,
+ /// which means that the property for all servers will be returned.
+ virtual int getProperty(const std::string& property_name,
+ const int property_value,
+ const BackendSelector& selector =
+ BackendSelector::UNSPEC(),
+ const ServerSelector& server_selector =
+ ServerSelector::ALL()) const {
+ int property;
+ getPropertyPtrConst<int, const std::string&, const int>
+ (&TestConfigBackend::getProperty, selector, server_selector, property,
+ property_name, property_value);
+ return (property);
+ }
+
+
/// @brief Retrieves multiple properties.
///
/// @param property_name Name of the properties which should be retrieved.
/// @param selector Backend selector. The default value of the selector
/// is @c UNSPEC which means that the properties will be searched in all
/// backends and the first non-empty list will be returned.
+ /// @param server_selector Server selector. The default value is set to @c ALL,
+ /// which means that the properties for all servers will be returned.
virtual PropertiesList getProperties(const std::string& property_name,
const BackendSelector& selector =
- BackendSelector::UNSPEC()) const {
+ BackendSelector::UNSPEC(),
+ const ServerSelector& server_selector =
+ ServerSelector::ALL()) const {
PropertiesList properties;
// If the selector is specified, this method will pick the appropriate
// the first non-empty list of properties in one of the backends.
// The template arguments specify the type of the list of properties
// and the argument of the getProperties method.
- getMultiplePropertiesConst<PropertiesList, const std::string&,
- &TestConfigBackend::getProperties>
- (properties, property_name, selector);
+ getMultiplePropertiesConst<PropertiesList, const std::string&>
+ (&TestConfigBackend::getProperties, selector, server_selector,
+ properties, property_name);
return (properties);
}
/// @param selector Backend selector. The default value of the selector
/// is @c UNSPEC which means that the properties will be searched in all
/// backends and the first non-empty list will be returned.
+ /// @param server_selector Server selector. The default value is set to @c ALL,
+ /// which means that the properties for all servers will be returned.
virtual PropertiesList getAllProperties(const BackendSelector& selector =
- BackendSelector::UNSPEC()) const {
+ BackendSelector::UNSPEC(),
+ const ServerSelector& server_selector =
+ ServerSelector::ALL()) const {
PropertiesList properties;
// This method is similar to getMultiplePropertiesConst but it lacks
// an argument and it simply returns all properties.
- getAllPropertiesConst<PropertiesList, &TestConfigBackend::getAllProperties>
- (properties, selector);
+ getAllPropertiesConst<PropertiesList>
+ (&TestConfigBackend::getAllProperties, selector, server_selector,
+ properties);
return (properties);
}
///
/// @param new_property New property to be added to a backend.
/// @param selector Backend selector. It has no default value.
+ /// @param server_selector The default value is @c ALL which means that
+ /// new property is going to be shared by all servers.
virtual void createProperty(const std::pair<std::string, int>& new_property,
- const BackendSelector& selector) {
- createUpdateDeleteProperty<const std::pair<std::string, int>&,
- &TestConfigBackend::createProperty>
- (new_property, selector);
+ const BackendSelector& selector,
+ const ServerSelector& server_selector =
+ ServerSelector::ALL()) {
+ createUpdateDeleteProperty<const std::pair<std::string, int>&>
+ (&TestConfigBackend::createProperty, selector, server_selector,
+ new_property);
}
-
};
using TestConfigBackendMgr = BaseConfigBackendMgr<TestConfigBackendPool>;
EXPECT_EQ(2, config_mgr_.getPool()->getProperty("cats",
BackendSelector(BackendSelector::Type::PGSQL)));
+ // Also make sure that the variant of getProperty function taking two arguments
+ // would return the value.
+ EXPECT_EQ(1, config_mgr_.getPool()->getProperty("dogs", 1,
+ BackendSelector(BackendSelector::Type::MYSQL)));
+
+ // If the value is not matching it should return 0.
+ EXPECT_EQ(0, config_mgr_.getPool()->getProperty("dogs", 2,
+ BackendSelector(BackendSelector::Type::MYSQL)));
+
// Try to use the backend that is not present.
EXPECT_THROW(config_mgr_.getPool()->getProperty("cats",
BackendSelector(BackendSelector::Type::CQL)),
--- /dev/null
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <config_backend/base_config_backend.h>
+#include <gtest/gtest.h>
+
+using namespace isc::cb;
+
+namespace {
+
+// Check that server selector can be set to UNASSIGNED.
+TEST(ServerSelectorTest, unassigned) {
+ ServerSelector selector = ServerSelector::UNASSIGNED();
+ EXPECT_EQ(ServerSelector::Type::UNASSIGNED, selector.getType());
+ EXPECT_TRUE(selector.getTags().empty());
+}
+
+// Check that server selector can be set to ALL.
+TEST(ServerSelectorTest, all) {
+ ServerSelector selector = ServerSelector::ALL();
+ EXPECT_EQ(ServerSelector::Type::ALL, selector.getType());
+ EXPECT_TRUE(selector.getTags().empty());
+}
+
+// Check that a single server can be selected.
+TEST(ServerSelectorTest, one) {
+ ServerSelector selector = ServerSelector::ONE("some-tag");
+ EXPECT_EQ(ServerSelector::Type::SUBSET, selector.getType());
+
+ std::set<std::string> tags = selector.getTags();
+ ASSERT_EQ(1, tags.size());
+ EXPECT_EQ(1, tags.count("some-tag"));
+}
+
+// Check that multiple servers can be selected.
+TEST(ServerSelectorTest, multiple) {
+ ServerSelector selector = ServerSelector::MULTIPLE({ "tag1", "tag2", "tag3" });
+ EXPECT_EQ(ServerSelector::Type::SUBSET, selector.getType());
+
+ std::set<std::string> tags = selector.getTags();
+ ASSERT_EQ(3, tags.size());
+ EXPECT_EQ(1, tags.count("tag1"));
+ EXPECT_EQ(1, tags.count("tag2"));
+ EXPECT_EQ(1, tags.count("tag3"));
+}
+
+}