Browse Source

[1067] DatabaseClient part of iteration

The implementation of the ZoneIterator for DatabaseClient (it isn't
publicly visible, it is hidden in the .cc file) and tests for it.
Michal 'vorner' Vaner 13 years ago
parent
commit
cce2a00af5
3 changed files with 312 additions and 7 deletions
  1. 97 1
      src/lib/datasrc/database.cc
  2. 2 1
      src/lib/datasrc/database.h
  3. 213 5
      src/lib/datasrc/tests/database_unittest.cc

+ 97 - 1
src/lib/datasrc/database.cc

@@ -13,11 +13,18 @@
 // PERFORMANCE OF THIS SOFTWARE.
 
 #include <datasrc/database.h>
+#include <datasrc/data_source.h>
+#include <datasrc/iterator.h>
 
 #include <exceptions/exceptions.h>
 #include <dns/name.h>
+#include <dns/rrclass.h>
+#include <dns/rrttl.h>
 
-using isc::dns::Name;
+#include <string>
+
+using namespace isc::dns;
+using std::string;
 
 namespace isc {
 namespace datasrc {
@@ -81,5 +88,94 @@ DatabaseClient::Finder::getClass() const {
     return isc::dns::RRClass::IN();
 }
 
+namespace {
+
+/*
+ * This needs, beside of converting all data from textual representation, group
+ * together rdata of the same RRsets. To do this, we hold one row of data ahead
+ * of iteration. When we get a request to provide data, we create it from this
+ * data and load a new one. If it is to be put to the same rrset, we add it.
+ * Otherwise we just return what we have and keep the row as the one ahead
+ * for next time.
+ */
+class Iterator : public ZoneIterator {
+public:
+    Iterator(const DatabaseConnection::IteratorContextPtr& context,
+             const RRClass& rrclass) :
+        context_(context),
+        class_(rrclass),
+        ready_(true)
+    {
+        // Prepare data for the next time
+        getData();
+    }
+    virtual isc::dns::ConstRRsetPtr getNextRRset() {
+        if (!ready_) {
+            isc_throw(isc::Unexpected, "Iterating past the zone end");
+        }
+        if (!data_ready_) {
+            // At the end of zone
+            ready_ = false;
+            return (ConstRRsetPtr());
+        }
+        string nameStr(name_), rtypeStr(rtype_);
+        int ttl(ttl_);
+        Name name(nameStr);
+        RRType rtype(rtypeStr);
+        RRsetPtr rrset(new RRset(name, class_, rtype, RRTTL(ttl)));
+        while (data_ready_ && name_ == nameStr && rtypeStr == rtype_) {
+            if (ttl_ != ttl) {
+                isc_throw(DataSourceError, "TTLs in rrset " + nameStr + "/" +
+                          rtypeStr + " differ");
+            }
+            rrset->addRdata(rdata::createRdata(rtype, class_, rdata_));
+            getData();
+        }
+        return (rrset);
+    }
+private:
+    // Load next row of data
+    void getData() {
+        data_ready_ = context_->getNext(name_, rtype_, ttl_, rdata_);
+    }
+    // The context
+    const DatabaseConnection::IteratorContextPtr context_;
+    // Class of the zone
+    RRClass class_;
+    // Status
+    bool ready_, data_ready_;
+    // Data of the next row
+    string name_, rtype_, rdata_;
+    int ttl_;
+};
+
+}
+
+ZoneIteratorPtr
+DatabaseClient::getIterator(const isc::dns::Name& name) const {
+    // Get the zone
+    std::pair<bool, int> zone(connection_->getZone(name));
+    if (!zone.first) {
+        // No such zone, can't continue
+        isc_throw(DataSourceError, "Zone " + name.toText() +
+                  " can not be iterated, because it doesn't exist "
+                  "in this data source");
+    }
+    // Request the context
+    DatabaseConnection::IteratorContextPtr
+        context(connection_->getIteratorContext(name, zone.second));
+    // It must not return NULL, that's a bug of the implementation
+    if (context == DatabaseConnection::IteratorContextPtr()) {
+        isc_throw(isc::Unexpected, "Iterator context null at " +
+                  name.toText());
+    }
+    // Create the iterator and return it
+    // TODO: Once #1062 is merged with this, we need to get the
+    // actual zone class from the connection, as the DatabaseClient
+    // doesn't know it and the iterator needs it (so it wouldn't query
+    // it each time)
+    return (ZoneIteratorPtr(new Iterator(context, RRClass::IN())));
+}
+
 }
 }

+ 2 - 1
src/lib/datasrc/database.h

@@ -271,7 +271,8 @@ public:
      *
      * \exception DataSourceError if the zone doesn't exist.
      * \exception isc::NotImplemented if the underlying DatabaseConnection
-     *     doesn't implement iteration.
+     *     doesn't implement iteration. But in case it is not implemented
+     *     and the zone doesn't exist, DataSourceError is thrown.
      * \exception Anything else the underlying DatabaseConnection might
      *     want to throw.
      * \param name The origin of the zone to iterate.

+ 213 - 5
src/lib/datasrc/tests/database_unittest.cc

@@ -15,36 +15,157 @@
 #include <gtest/gtest.h>
 
 #include <dns/name.h>
+#include <dns/rrttl.h>
 #include <exceptions/exceptions.h>
 
 #include <datasrc/database.h>
+#include <datasrc/data_source.h>
+#include <datasrc/iterator.h>
 
 using namespace isc::datasrc;
 using namespace std;
 using namespace boost;
-using isc::dns::Name;
+using namespace isc::dns;
 
 namespace {
 
 /*
- * A virtual database connection that pretends it contains single zone --
- * example.org.
+ * A connection with minimum implementation, keeping the original
+ * "NotImplemented" methods.
  */
-class MockConnection : public DatabaseConnection {
+class NopConnection : public DatabaseConnection {
 public:
     virtual std::pair<bool, int> getZone(const Name& name) const {
         if (name == Name("example.org")) {
             return (std::pair<bool, int>(true, 42));
+        } else if (name == Name("null.example.org")) {
+            return (std::pair<bool, int>(true, 13));
+        } else if (name == Name("empty.example.org")) {
+            return (std::pair<bool, int>(true, 0));
+        } else if (name == Name("bad.example.org")) {
+            return (std::pair<bool, int>(true, -1));
         } else {
             return (std::pair<bool, int>(false, 0));
         }
     }
 };
 
+/*
+ * A virtual database connection that pretends it contains single zone --
+ * example.org.
+ *
+ * It has the same getZone method as NopConnection, but it provides
+ * implementation of the optional functionality.
+ */
+class MockConnection : public NopConnection {
+private:
+    class MockIteratorContext : public IteratorContext {
+    private:
+        int step;
+    public:
+        MockIteratorContext() :
+            step(0)
+        { }
+        virtual bool getNext(string& name, string& rtype, int& ttl,
+                             string& data)
+        {
+            switch (step ++) {
+                case 0:
+                    name = "example.org";
+                    rtype = "SOA";
+                    ttl = 300;
+                    data = "ns1.example.org. admin.example.org. "
+                        "1234 3600 1800 2419200 7200";
+                    return (true);
+                case 1:
+                    name = "x.example.org";
+                    rtype = "A";
+                    ttl = 300;
+                    data = "192.0.2.1";
+                    return (true);
+                case 2:
+                    name = "x.example.org";
+                    rtype = "A";
+                    ttl = 300;
+                    data = "192.0.2.2";
+                    return (true);
+                case 3:
+                    name = "x.example.org";
+                    rtype = "AAAA";
+                    ttl = 300;
+                    data = "2001:db8::1";
+                    return (true);
+                case 4:
+                    name = "x.example.org";
+                    rtype = "AAAA";
+                    ttl = 300;
+                    data = "2001:db8::2";
+                    return (true);
+                default:
+                    ADD_FAILURE() <<
+                        "Request past the end of iterator context";
+                case 5:
+                    return (false);
+            }
+        }
+    };
+    class EmptyIteratorContext : public IteratorContext {
+    public:
+        virtual bool getNext(string&, string&, int&, string&) {
+            return (false);
+        }
+    };
+    class BadIteratorContext : public IteratorContext {
+    private:
+        int step;
+    public:
+        BadIteratorContext() :
+            step(0)
+        { }
+        virtual bool getNext(string& name, string& rtype, int& ttl,
+                             string& data)
+        {
+            switch (step ++) {
+                case 0:
+                    name = "x.example.org";
+                    rtype = "A";
+                    ttl = 300;
+                    data = "192.0.2.1";
+                    return (true);
+                case 1:
+                    name = "x.example.org";
+                    rtype = "A";
+                    ttl = 301;
+                    data = "192.0.2.2";
+                    return (true);
+                default:
+                    ADD_FAILURE() <<
+                        "Request past the end of iterator context";
+                case 2:
+                    return (false);
+            }
+        }
+    };
+public:
+    virtual IteratorContextPtr getIteratorContext(const Name&, int id) const {
+        if (id == 42) {
+            return (IteratorContextPtr(new MockIteratorContext()));
+        } else if (id == 13) {
+            return (IteratorContextPtr());
+        } else if (id == 0) {
+            return (IteratorContextPtr(new EmptyIteratorContext()));
+        } else if (id == -1) {
+            return (IteratorContextPtr(new BadIteratorContext()));
+        } else {
+            isc_throw(isc::Unexpected, "Unknown zone ID");
+        }
+    }
+};
+
 // This tests the default getIteratorContext behaviour, throwing NotImplemented
 TEST(DatabaseConnectionTest, getIteratorContext) {
     // The parameters don't matter
-    EXPECT_THROW(MockConnection().getIteratorContext(Name("."), 1),
+    EXPECT_THROW(NopConnection().getIteratorContext(Name("."), 1),
                  isc::NotImplemented);
 }
 
@@ -103,4 +224,91 @@ TEST_F(DatabaseClientTest, noConnException) {
                  isc::InvalidParameter);
 }
 
+// If the zone doesn't exist, exception is thrown
+TEST_F(DatabaseClientTest, noZoneIterator) {
+    EXPECT_THROW(client_->getIterator(Name("example.com")), DataSourceError);
+}
+
+// If the zone doesn't exist and iteration is not implemented, it still throws
+// the exception it doesn't exist
+TEST_F(DatabaseClientTest, noZoneNotImplementedIterator) {
+    EXPECT_THROW(DatabaseClient(auto_ptr<DatabaseConnection>(
+        new NopConnection())).getIterator(Name("example.com")),
+                 DataSourceError);
+}
+
+TEST_F(DatabaseClientTest, notImplementedIterator) {
+    EXPECT_THROW(DatabaseClient(auto_ptr<DatabaseConnection>(
+        new NopConnection())).getIterator(Name("example.org")),
+                 isc::NotImplemented);
+}
+
+// Pretend a bug in the connection and pass NULL as the context
+// Should not crash, but gracefully throw
+TEST_F(DatabaseClientTest, nullIteratorContext) {
+    EXPECT_THROW(client_->getIterator(Name("null.example.org")),
+                 isc::Unexpected);
+}
+
+// It doesn't crash or anything if the zone is completely empty
+TEST_F(DatabaseClientTest, emptyIterator) {
+    ZoneIteratorPtr it(client_->getIterator(Name("empty.example.org")));
+    EXPECT_EQ(ConstRRsetPtr(), it->getNextRRset());
+    // This is past the end, it should throw
+    EXPECT_THROW(it->getNextRRset(), isc::Unexpected);
+}
+
+// Iterate trough a zone
+TEST_F(DatabaseClientTest, iterator) {
+    ZoneIteratorPtr it(client_->getIterator(Name("example.org")));
+    ConstRRsetPtr rrset(it->getNextRRset());
+    ASSERT_NE(ConstRRsetPtr(), rrset);
+    EXPECT_EQ(Name("example.org"), rrset->getName());
+    EXPECT_EQ(RRClass::IN(), rrset->getClass());
+    EXPECT_EQ(RRType::SOA(), rrset->getType());
+    EXPECT_EQ(RRTTL(300), rrset->getTTL());
+    RdataIteratorPtr rit(rrset->getRdataIterator());
+    ASSERT_FALSE(rit->isLast());
+    rit->next();
+    EXPECT_TRUE(rit->isLast());
+
+    rrset = it->getNextRRset();
+    ASSERT_NE(ConstRRsetPtr(), rrset);
+    EXPECT_EQ(Name("x.example.org"), rrset->getName());
+    EXPECT_EQ(RRClass::IN(), rrset->getClass());
+    EXPECT_EQ(RRType::A(), rrset->getType());
+    EXPECT_EQ(RRTTL(300), rrset->getTTL());
+    rit = rrset->getRdataIterator();
+    ASSERT_FALSE(rit->isLast());
+    EXPECT_EQ("192.0.2.1", rit->getCurrent().toText());
+    rit->next();
+    ASSERT_FALSE(rit->isLast());
+    EXPECT_EQ("192.0.2.2", rit->getCurrent().toText());
+    rit->next();
+    EXPECT_TRUE(rit->isLast());
+
+    rrset = it->getNextRRset();
+    ASSERT_NE(ConstRRsetPtr(), rrset);
+    EXPECT_EQ(Name("x.example.org"), rrset->getName());
+    EXPECT_EQ(RRClass::IN(), rrset->getClass());
+    EXPECT_EQ(RRType::AAAA(), rrset->getType());
+    EXPECT_EQ(RRTTL(300), rrset->getTTL());
+    EXPECT_EQ(ConstRRsetPtr(), it->getNextRRset());
+    rit = rrset->getRdataIterator();
+    ASSERT_FALSE(rit->isLast());
+    EXPECT_EQ("2001:db8::1", rit->getCurrent().toText());
+    rit->next();
+    ASSERT_FALSE(rit->isLast());
+    EXPECT_EQ("2001:db8::2", rit->getCurrent().toText());
+    rit->next();
+    EXPECT_TRUE(rit->isLast());
+}
+
+// This has inconsistent TTL in the set (the rest, like nonsense in
+// the data is handled in rdata itself).
+TEST_F(DatabaseClientTest, badIterator) {
+    ZoneIteratorPtr it(client_->getIterator(Name("bad.example.org")));
+    EXPECT_THROW(it->getNextRRset(), DataSourceError);
+}
+
 }