DnsConfigService and a posix implementation

Contributed by: Szymon Jakubczak <[email protected]>

BUG=90881

TEST=./net_unittests --gtest_filter="DnsConfigServiceTest*"


Review URL: http://codereview.chromium.org/7518028

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@97282 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/net/dns/dns_config_service_posix_unittest.cc b/net/dns/dns_config_service_posix_unittest.cc
new file mode 100644
index 0000000..9c81e14
--- /dev/null
+++ b/net/dns/dns_config_service_posix_unittest.cc
@@ -0,0 +1,472 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <arpa/inet.h>
+#include <resolv.h>
+
+#include "base/bind.h"
+#include "base/compiler_specific.h"
+#include "base/message_loop.h"
+#include "base/message_loop_proxy.h"
+#include "base/synchronization/waitable_event.h"
+#include "base/threading/thread.h"
+#include "net/base/ip_endpoint.h"
+#include "net/dns/dns_config_service_posix.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace net {
+
+namespace {
+
+void CompareConfig(const struct __res_state &res, const DnsConfig& config) {
+  EXPECT_EQ(config.ndots, static_cast<int>(res.ndots));
+  EXPECT_EQ(config.edns0, (res.options & RES_USE_EDNS0) != 0);
+  EXPECT_EQ(config.rotate, (res.options & RES_ROTATE) != 0);
+  EXPECT_EQ(config.timeout.InSeconds(), res.retrans);
+  EXPECT_EQ(config.attempts, res.retry);
+
+  // Compare nameservers. IPv6 precede IPv4.
+#if OS_LINUX
+  size_t nscount6 = res._u._ext.nscount6;
+#else
+  size_t nscount6 = 0;
+#endif
+  size_t nscount4 = res.nscount;
+  ASSERT_EQ(config.nameservers.size(), nscount6 + nscount4);
+#if OS_LINUX
+  for (size_t i = 0; i < nscount6; ++i) {
+    IPEndPoint ipe;
+    EXPECT_TRUE(ipe.FromSockAddr(
+        reinterpret_cast<const struct sockaddr*>(res._u._ext.nsaddrs[i]),
+        sizeof *res._u._ext.nsaddrs[i]));
+    EXPECT_EQ(config.nameservers[i], ipe);
+  }
+#endif
+  for (size_t i = 0; i < nscount4; ++i) {
+    IPEndPoint ipe;
+    EXPECT_TRUE(ipe.FromSockAddr(
+        reinterpret_cast<const struct sockaddr*>(&res.nsaddr_list[i]),
+        sizeof res.nsaddr_list[i]));
+    EXPECT_EQ(config.nameservers[nscount6 + i], ipe);
+  }
+
+  ASSERT_TRUE(config.search.size() <= MAXDNSRCH);
+  EXPECT_TRUE(res.dnsrch[config.search.size()] == NULL);
+  for (size_t i = 0; i < config.search.size(); ++i) {
+    EXPECT_EQ(config.search[i], res.dnsrch[i]);
+  }
+}
+
+// Fills in |res| with sane configuration. Change |generation| to add diversity.
+void InitializeResState(res_state res, int generation) {
+  memset(res, 0, sizeof(*res));
+  res->options = RES_INIT | RES_ROTATE;
+  res->ndots = 2;
+  res->retrans = 8;
+  res->retry = 7;
+
+  const char kDnsrch[] = "chromium.org" "\0" "example.com";
+  memcpy(res->defdname, kDnsrch, sizeof(kDnsrch));
+  res->dnsrch[0] = res->defdname;
+  res->dnsrch[1] = res->defdname + sizeof("chromium.org");
+
+  const char* ip4addr[3] = {
+      "8.8.8.8",
+      "192.168.1.1",
+      "63.1.2.4",
+  };
+
+  for (int i = 0; i < 3; ++i) {
+    struct sockaddr_in sa;
+    sa.sin_family = AF_INET;
+    sa.sin_port = htons(NS_DEFAULTPORT + i - generation);
+    inet_pton(AF_INET, ip4addr[i], &sa.sin_addr);
+    res->nsaddr_list[i] = sa;
+  }
+  res->nscount = 3;
+
+#if OS_LINUX
+  const char* ip6addr[2] = {
+      "2001:db8:0::42",
+      "::FFFF:129.144.52.38",
+  };
+
+  for (int i = 0; i < 2; ++i) {
+    // Must use malloc to mimick res_ninit.
+    struct sockaddr_in6 *sa6;
+    sa6 = (struct sockaddr_in6 *)malloc(sizeof(*sa6));
+    sa6->sin6_family = AF_INET6;
+    sa6->sin6_port = htons(NS_DEFAULTPORT - i);
+    inet_pton(AF_INET6, ip6addr[i], &sa6->sin6_addr);
+    res->_u._ext.nsaddrs[i] = sa6;
+  }
+  res->_u._ext.nscount6 = 2;
+#endif
+}
+
+void CloseResState(res_state res) {
+#if OS_LINUX
+  for (int i = 0; i < res->_u._ext.nscount6; ++i) {
+    ASSERT_TRUE(res->_u._ext.nsaddrs[i] != NULL);
+    free(res->_u._ext.nsaddrs[i]);
+  }
+#endif
+}
+
+class DnsConfigServiceTest : public testing::Test,
+                             public DnsConfigService::Observer {
+ public:
+  // Mocks
+
+  // DnsConfigService owns the instances of ResolverLib and
+  // FilePathWatcherFactory that it gets, so use simple proxies to call
+  // DnsConfigServiceTest.
+
+  // ResolverLib is owned by WatcherDelegate which is posted to WorkerPool so
+  // it must be canceled before the test is over.
+  class MockResolverLib : public DnsConfigServicePosix::ResolverLib {
+   public:
+    explicit MockResolverLib(DnsConfigServiceTest *test) : test_(test) {}
+    virtual ~MockResolverLib() {
+      base::AutoLock lock(lock_);
+      if (test_) {
+        EXPECT_TRUE(test_->IsComplete());
+      }
+    }
+    virtual int ninit(res_state res) OVERRIDE {
+      base::AutoLock lock(lock_);
+      if (test_)
+        return test_->OnNinit(res);
+      else
+        return -1;
+    }
+    virtual void nclose(res_state res) OVERRIDE {
+      CloseResState(res);
+    }
+    void Cancel() {
+      base::AutoLock lock(lock_);
+      test_ = NULL;
+    }
+   private:
+    base::Lock lock_;
+    DnsConfigServiceTest *test_;
+  };
+
+  class MockFilePathWatcherShim
+    : public DnsConfigServicePosix::FilePathWatcherShim {
+   public:
+    typedef base::files::FilePathWatcher::Delegate Delegate;
+
+    explicit MockFilePathWatcherShim(DnsConfigServiceTest* t) : test_(t) {}
+    virtual ~MockFilePathWatcherShim() {
+      test_->OnShimDestroyed(this);
+    }
+
+    // Enforce one-Watch-per-lifetime as the original FilePathWatcher
+    virtual bool Watch(const FilePath& path,
+                       Delegate* delegate) OVERRIDE {
+      EXPECT_TRUE(path_.empty()) << "Only one-Watch-per-lifetime allowed.";
+      EXPECT_TRUE(!delegate_.get());
+      path_ = path;
+      delegate_ = delegate;
+      return test_->OnWatch();
+    }
+
+    void PathChanged() {
+      delegate_->OnFilePathChanged(path_);
+    }
+
+    void PathError() {
+      delegate_->OnFilePathError(path_);
+    }
+
+   private:
+    FilePath path_;
+    scoped_refptr<Delegate> delegate_;
+    DnsConfigServiceTest* test_;
+  };
+
+  class MockFilePathWatcherFactory
+    : public DnsConfigServicePosix::FilePathWatcherFactory {
+   public:
+    explicit MockFilePathWatcherFactory(DnsConfigServiceTest* t) : test(t) {}
+    virtual ~MockFilePathWatcherFactory() {
+      EXPECT_TRUE(test->IsComplete());
+    }
+    virtual DnsConfigServicePosix::FilePathWatcherShim*
+        CreateFilePathWatcher() OVERRIDE {
+      return test->CreateFilePathWatcher();
+    }
+    DnsConfigServiceTest* test;
+  };
+
+  // Helpers for mocks.
+
+  DnsConfigServicePosix::FilePathWatcherShim* CreateFilePathWatcher() {
+    watcher_shim_ = new MockFilePathWatcherShim(this);
+    return watcher_shim_;
+  }
+
+  void OnShimDestroyed(MockFilePathWatcherShim* destroyed_shim) {
+    // Precaution to avoid segfault.
+    if (watcher_shim_ == destroyed_shim)
+      watcher_shim_ = NULL;
+  }
+
+  // On each event, post QuitTask to allow use of MessageLoop::Run() to
+  // synchronize the threads.
+
+  bool OnWatch() {
+    EXPECT_TRUE(message_loop_ == MessageLoop::current());
+    watch_called_ = true;
+    BreakNow("OnWatch");
+    return !fail_on_watch_;
+  }
+
+  int OnNinit(res_state res) {
+    { // Check that res_ninit is executed serially.
+      base::AutoLock lock(ninit_lock_);
+      EXPECT_FALSE(ninit_running_) << "res_ninit is not called serially!";
+      ninit_running_ = true;
+    }
+    BreakNow("OnNinit");
+    ninit_allowed_.Wait();
+    // Calling from another thread is a bit dirty, but it's protected.
+    int rv = OnNinitNonThreadSafe(res);
+    // This lock might be destroyed after ninit_called_ is signalled.
+    {
+      base::AutoLock lock(ninit_lock_);
+      ninit_running_ = false;
+    }
+    ninit_called_.Signal();
+    return rv;
+  }
+
+  virtual void OnConfigChanged(const DnsConfig& new_config) OVERRIDE {
+    EXPECT_TRUE(message_loop_ == MessageLoop::current());
+    CompareConfig(res_, new_config);
+    EXPECT_FALSE(new_config.Equals(last_config_)) <<
+        "Config must be different from last call.";
+    last_config_ = new_config;
+    got_config_ = true;
+    BreakNow("OnConfigChanged");
+  }
+
+  bool IsComplete() {
+    return complete_;
+  }
+
+ protected:
+  friend class BreakTask;
+  class BreakTask : public Task {
+   public:
+    BreakTask(DnsConfigServiceTest* test, std::string breakpoint)
+      : test_(test), breakpoint_(breakpoint) {}
+    virtual ~BreakTask() {}
+    virtual void Run() OVERRIDE {
+      test_->breakpoint_ = breakpoint_;
+      MessageLoop::current()->QuitNow();
+    }
+   private:
+    DnsConfigServiceTest* test_;
+    std::string breakpoint_;
+  };
+
+  void BreakNow(std::string b) {
+    message_loop_->PostTask(FROM_HERE, new BreakTask(this, b));
+  }
+
+  void RunUntilBreak(std::string b) {
+    message_loop_->Run();
+    ASSERT_EQ(breakpoint_, b);
+  }
+
+  DnsConfigServiceTest()
+      : res_lib_(new MockResolverLib(this)),
+        watcher_shim_(NULL),
+        res_generation_(1),
+        watch_called_(false),
+        got_config_(false),
+        fail_on_watch_(false),
+        fail_on_ninit_(false),
+        complete_(false),
+        ninit_allowed_(false, false),
+        ninit_called_(false, false),
+        ninit_running_(false) {
+  }
+
+  // This is on WorkerPool, but protected by ninit_allowed_.
+  int OnNinitNonThreadSafe(res_state res) {
+    if (fail_on_ninit_)
+      return -1;
+    InitializeResState(res, res_generation_);
+    // Store a (deep) copy in the fixture to later verify correctness.
+    CloseResState(&res_);
+    InitializeResState(&res_, res_generation_);
+    return 0;
+  }
+
+  // Helpers for tests.
+
+  // Lets OnNinit run OnNinitNonThreadSafe and waits for it to complete.
+  // Might get OnConfigChanged scheduled on the loop but we have no certainty.
+  void WaitForNinit() {
+    RunUntilBreak("OnNinit");
+    ninit_allowed_.Signal();
+    ninit_called_.Wait();
+  }
+
+  // test::Test methods
+  virtual void SetUp() OVERRIDE {
+    message_loop_ = MessageLoop::current();
+    service_.reset(new DnsConfigServicePosix());
+    service_->set_resolver_lib(res_lib_);
+    service_->set_watcher_factory(new MockFilePathWatcherFactory(this));
+    memset(&res_, 0, sizeof(res_));
+  }
+
+  virtual void TearDown() OVERRIDE {
+    // res_lib_ could outlive the test, so make sure it doesn't call it.
+    res_lib_->Cancel();
+    CloseResState(&res_);
+    // Allow service_ to clean up ResolverLib and FilePathWatcherFactory.
+    complete_ = true;
+  }
+
+  MockResolverLib* res_lib_;
+  MockFilePathWatcherShim* watcher_shim_;
+  // Adds variety to the content of res_state.
+  int res_generation_;
+  struct __res_state res_;
+  DnsConfig last_config_;
+
+  bool watch_called_;
+  bool got_config_;
+  bool fail_on_watch_;
+  bool fail_on_ninit_;
+  bool complete_;
+
+  // Ninit is called on WorkerPool so we need to synchronize with it.
+  base::WaitableEvent ninit_allowed_;
+  base::WaitableEvent ninit_called_;
+
+  // Protected by ninit_lock_. Used to verify that Ninit calls are serialized.
+  bool ninit_running_;
+  base::Lock ninit_lock_;
+
+  // Loop for this thread.
+  MessageLoop* message_loop_;
+
+  // Service under test.
+  scoped_ptr<DnsConfigServicePosix> service_;
+
+  std::string breakpoint_;
+};
+
+TEST(DnsConfigTest, ResolverConfigConvertAndEquals) {
+  struct __res_state res[2];
+  DnsConfig config[2];
+  for (int i = 0; i < 2; ++i) {
+    InitializeResState(&res[i], i);
+    ASSERT_TRUE(ConvertResToConfig(res[i], &config[i]));
+  }
+  for (int i = 0; i < 2; ++i) {
+    CompareConfig(res[i], config[i]);
+    CloseResState(&res[i]);
+  }
+  EXPECT_TRUE(config[0].Equals(config[0]));
+  EXPECT_FALSE(config[0].Equals(config[1]));
+  EXPECT_FALSE(config[1].Equals(config[0]));
+}
+
+TEST_F(DnsConfigServiceTest, FilePathWatcherFailures) {
+  // For these tests, disable ninit.
+  res_lib_->Cancel();
+
+  fail_on_watch_ = true;
+  service_->Watch();
+  RunUntilBreak("OnWatch");
+  EXPECT_TRUE(watch_called_) << "Must call FilePathWatcher::Watch().";
+
+  fail_on_watch_ = false;
+  watch_called_ = false;
+  RunUntilBreak("OnWatch");  // Due to backoff this will take 100ms.
+  EXPECT_TRUE(watch_called_) <<
+      "Must restart on FilePathWatcher::Watch() failure.";
+
+  watch_called_ = false;
+  ASSERT_TRUE(watcher_shim_);
+  watcher_shim_->PathError();
+  RunUntilBreak("OnWatch");
+  EXPECT_TRUE(watch_called_) <<
+      "Must restart on FilePathWatcher::Delegate::OnFilePathError().";
+
+  // Worker thread could still be posting OnResultAvailable to the message loop
+}
+
+TEST_F(DnsConfigServiceTest, NotifyOnValidAndDistinctConfig) {
+  service_->AddObserver(this);
+  service_->Watch();
+  RunUntilBreak("OnWatch");
+  fail_on_ninit_ = true;
+  WaitForNinit();
+
+  // If OnNinit posts OnResultAvailable before the next call, then this test
+  // verifies that failure on ninit should not cause OnConfigChanged.
+  // Otherwise, this only verifies that ninit calls are serialized.
+
+  fail_on_ninit_ = false;
+  ASSERT_TRUE(watcher_shim_);
+  watcher_shim_->PathChanged();
+  WaitForNinit();
+
+  RunUntilBreak("OnConfigChanged");
+  EXPECT_TRUE(got_config_);
+
+  message_loop_->AssertIdle();
+
+  got_config_ = false;
+  // Forget about the config to test if we get it again on AddObserver.
+  last_config_ = DnsConfig();
+  service_->RemoveObserver(this);
+  service_->AddObserver(this);
+  RunUntilBreak("OnConfigChanged");
+  EXPECT_TRUE(got_config_) << "Did not get config after AddObserver.";
+
+  // Simulate spurious FilePathChanged.
+  ASSERT_TRUE(watcher_shim_);
+  watcher_shim_->PathChanged();
+  WaitForNinit();
+
+  // OnConfigChanged will catch that the config did not actually change.
+
+  got_config_ = false;
+  ++res_generation_;
+  ASSERT_TRUE(watcher_shim_);
+  watcher_shim_->PathChanged();
+  WaitForNinit();
+  RunUntilBreak("OnConfigChanged");
+  EXPECT_TRUE(got_config_) << "Did not get config after change";
+
+  message_loop_->AssertIdle();
+
+  // Schedule two calls. OnNinit checks if it is called serially.
+  ++res_generation_;
+  ASSERT_TRUE(watcher_shim_);
+  watcher_shim_->PathChanged();
+  // ninit is blocked, so this will have to induce read_pending
+  watcher_shim_->PathChanged();
+  WaitForNinit();
+  WaitForNinit();
+  RunUntilBreak("OnConfigChanged");
+  EXPECT_TRUE(got_config_) << "Did not get config after change";
+
+  // We should be done with all tasks.
+  message_loop_->AssertIdle();
+}
+
+}  // namespace
+
+}  // namespace net
+