Updated Speedtest CLI for latest Speedtest server changes

- Load server list from the global server
- Allow override for global server and server list
- Load/parse JSON config responses
- Load speedtest config form the speedtest server
- Split upload/download/ping into separate tests
- More verbose output
- Correctly determine transfer ports from server config

Change-Id: I99d3b3b8e1a3a5ade732e58478f61ddc1aae0ce2
diff --git a/speedtest/Makefile b/speedtest/Makefile
index 56dc3e1..b3b625c 100644
--- a/speedtest/Makefile
+++ b/speedtest/Makefile
@@ -11,20 +11,52 @@
 GMOCK_DIR=googlemock
 TFLAGS=$(DEBUG) -isystem ${GTEST_DIR}/include -isystem $(GMOCK_DIR)/include -pthread -std=c++11
 
-LIBS=-lcurl -lpthread
+LIBS=-lcurl -lpthread -ljsoncpp
 TOBJS=curl_env.o url.o errors.o request.o utils.o
-OBJS=errors.o curl_env.o options.o request.o utils.o speedtest.o url.o
+OBJS=config.o \
+     curl_env.o \
+     download_test.o \
+     errors.o \
+     generic_test.o \
+     options.o \
+     ping_test.o \
+     request.o \
+     runner.o \
+     speedtest.o \
+     transfer_test.o \
+     upload_test.o \
+     url.o \
+     utils.o
 
 all: speedtest
 
+config.o: config.cc config.h url.h
 errors.o: errors.cc errors.h
 curl_env.o: curl_env.cc curl_env.h errors.h request.h
+download_test.o: download_test.cc download_test.h generic_test.h transfer_test.h utils.h
+generic_test.o: generic_test.cc generic_test.h request.h utils.h
 options.o: options.cc options.h url.h
-utils.o: utils.cc options.h
-request.o: request.cc request.h curl_env.h url.h
-url.o: url.cc url.h
-speedtest.o: speedtest.cc speedtest.h curl_env.h options.h request.h url.h
+ping_test.o: ping_test.cc ping_test.h generic_test.h request.h url.h utils.h
+request.o: request.cc request.h url.h
+runner.o: runner.cc runner.h generic_test.h transfer_test.h
+speedtest.o: speedtest.cc \
+             speedtest.h \
+             config.h \
+             curl_env.h \
+             download_test.h \
+             generic_test.h \
+             options.h \
+             ping_test.h \
+             request.h \
+             runner.h \
+             upload_test.h \
+             url.h
 speedtest_main.o: speedtest_main.cc options.h speedtest.h
+transfer_test.o: transfer_test.cc transfer_test.h generic_test.h
+upload_test.o: upload_test.cc upload_test.h generic_test.h transfer_test.h utils.h
+utils.o: utils.cc options.h
+url.o: url.cc url.h utils.h
+
 speedtest: speedtest_main.o $(OBJS)
 	$(CXX) -o $@ $< $(OBJS) $(LDFLAGS) $(LIBS)
 
@@ -45,14 +77,14 @@
 libspeedtesttest.a: $(TOBJS)
 	ar -rv libspeedtesttest.a $(TOBJS)
 
-%_test.o: %_test.cc %.h
+%_test.o: %_test.cc %.h %.cc
 	$(CXX) -c $< $(TFLAGS) $(CXXFLAGS)
 
 %_test: %_test.o %.o libgmock.a libspeedtesttest.a
 	$(CXX) -o $@ $(TFLAGS) googlemock/src/gmock_main.cc $< $*.o $(LDFLAGS) $(LIBS) libgmock.a libspeedtesttest.a
 	./$@
 
-test: options_test request_test url_test
+test: config_test options_test request_test url_test
 
 install: speedtest
 	$(INSTALL) -m 0755 speedtest $(BINDIR)/
diff --git a/speedtest/config.cc b/speedtest/config.cc
new file mode 100644
index 0000000..d12b4e5
--- /dev/null
+++ b/speedtest/config.cc
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "config.h"
+
+// For some reason, the libjsoncpp package installs to /usr/include/jsoncpp/json
+// instead of /usr{,/local}/include/json
+#include <jsoncpp/json/json.h>
+
+namespace speedtest {
+
+bool ParseConfig(const std::string &json, Config *config) {
+  if (!config) {
+    return false;
+  }
+
+  Json::Reader reader;
+  Json::Value root;
+  if (!reader.parse(json, root, false)) {
+    return false;
+  }
+
+  config->download_size = root["downloadSize"].asInt();
+  config->upload_size = root["uploadSize"].asInt();
+  config->interval_size = root["intervalSize"].asInt();
+  config->location_name = root["locationName"].asString();
+  config->min_transfer_intervals = root["minTransferIntervals"].asInt();
+  config->max_transfer_intervals = root["maxTransferIntervals"].asInt();
+  config->min_transfer_runtime = root["minTransferRunTime"].asInt();
+  config->max_transfer_runtime = root["maxTransferRunTime"].asInt();
+  config->max_transfer_variance = root["maxTransferVariance"].asDouble();
+  config->num_uploads = root["numConcurrentUploads"].asInt();
+  config->num_downloads = root["numConcurrentDownloads"].asInt();
+  config->ping_runtime = root["pingRunTime"].asInt();
+  config->ping_timeout = root["pingTimeout"].asInt();
+  config->transfer_port_start = root["transferPortStart"].asInt();
+  config->transfer_port_end = root["transferPortEnd"].asInt();
+  return true;
+}
+
+bool ParseServers(const std::string &json, std::vector<http::Url> *servers) {
+  if (!servers) {
+    return false;
+  }
+
+  Json::Reader reader;
+  Json::Value root;
+  if (!reader.parse(json, root, false)) {
+    return false;
+  }
+
+  for (const auto &it : root["regionalServers"]) {
+    http::Url url(it.asString());
+    if (!url.ok()) {
+      return false;
+    }
+    servers->emplace_back(url);
+  }
+  return true;
+}
+
+void PrintConfig(const Config &config) {
+  PrintConfig(std::cout, config);
+}
+
+void PrintConfig(std::ostream &out, const Config &config) {
+  out << "Download size: " << config.download_size << " bytes\n"
+      << "Upload size: " << config.upload_size << " bytes\n"
+      << "Interval size: " << config.interval_size << " ms\n"
+      << "Location name: " << config.location_name << "\n"
+      << "Min transfer intervals: " << config.min_transfer_intervals << "\n"
+      << "Max transfer intervals: " << config.max_transfer_intervals << "\n"
+      << "Min transfer runtime: " << config.min_transfer_runtime << " ms\n"
+      << "Max transfer runtime: " << config.max_transfer_runtime << " ms\n"
+      << "Max transfer variance: " << config.max_transfer_variance << "\n"
+      << "Number of downloads: " << config.num_downloads << "\n"
+      << "Number of uploads: " << config.num_uploads << "\n"
+      << "Ping runtime: " << config.ping_runtime << " ms\n"
+      << "Ping timeout: " << config.ping_timeout << " ms\n"
+      << "Transfer port start: " << config.transfer_port_start << "\n"
+      << "Transfer port end: " << config.transfer_port_end << "\n";
+}
+
+}  // namespace
diff --git a/speedtest/config.h b/speedtest/config.h
new file mode 100644
index 0000000..01b5483
--- /dev/null
+++ b/speedtest/config.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SPEEDTEST_CONFIG_H
+#define SPEEDTEST_CONFIG_H
+
+#include <iostream>
+#include <string>
+#include <vector>
+#include "url.h"
+
+namespace speedtest {
+
+struct Config {
+  int download_size = 0;
+  int upload_size = 0;
+  int interval_size = 0;
+  std::string location_name;
+  int min_transfer_intervals = 0;
+  int max_transfer_intervals = 0;
+  int min_transfer_runtime = 0;
+  int max_transfer_runtime = 0;
+  double max_transfer_variance = 0;
+  int num_downloads = 0;
+  int num_uploads = 0;
+  int ping_runtime = 0;
+  int ping_timeout = 0;
+  int transfer_port_start = 0;
+  int transfer_port_end = 0;
+};
+
+// Parses a JSON document into a config struct.
+// Returns true with the config struct populated on success.
+// Returns false if the JSON is invalid or config is null.
+bool ParseConfig(const std::string &json, Config *config);
+
+// Parses a JSON document into a list of server URLs
+// Returns true with the servers populated in the vector on success.
+// Returns false if the JSON is invalid or servers is null.
+bool ParseServers(const std::string &json, std::vector<http::Url> *servers);
+
+void PrintConfig(const Config &config);
+void PrintConfig(std::ostream &out, const Config &config);
+
+}  // namespace speedtest
+
+#endif //SPEEDTEST_CONFIG_H
diff --git a/speedtest/config_test.cc b/speedtest/config_test.cc
new file mode 100644
index 0000000..681e0fc
--- /dev/null
+++ b/speedtest/config_test.cc
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "config.h"
+
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+namespace speedtest {
+namespace {
+
+const char *kValidConfig = R"CONFIG(
+{
+    "downloadSize": 10000000,
+    "intervalSize": 200,
+    "locationName": "Kansas City",
+    "maxTransferIntervals": 25,
+    "maxTransferRunTime": 20000,
+    "maxTransferVariance": 0.08,
+    "minTransferIntervals": 10,
+    "minTransferRunTime": 5000,
+    "numConcurrentDownloads": 20,
+    "numConcurrentUploads": 15,
+    "pingRunTime": 3000,
+    "pingTimeout": 300,
+    "transferPortEnd": 3023,
+    "transferPortStart": 3004,
+    "uploadSize": 20000000
+}
+)CONFIG";
+
+const char *kValidServers = R"SERVERS(
+{
+    "locationName": "Kansas City",
+    "regionalServers": [
+        "http://austin.speed.googlefiber.net/",
+        "http://kansas.speed.googlefiber.net/",
+        "http://provo.speed.googlefiber.net/",
+        "http://stanford.speed.googlefiber.net/"
+    ]
+}
+)SERVERS";
+
+const char *kInvalidServers = R"SERVERS(
+{
+    "locationName": "Kansas City",
+    "regionalServers": [
+        "example.com..",
+    ]
+}
+)SERVERS";
+
+const char *kInvalidJson = "{{}{";
+
+TEST(ParseConfigTest, NullConfig_Invalid) {
+  EXPECT_FALSE(ParseConfig(kValidConfig, nullptr));
+}
+
+TEST(ParseConfigTest, EmptyJson_Invalid) {
+  Config config;
+  EXPECT_FALSE(ParseConfig("", &config));
+}
+
+TEST(ParseConfigTest, InvalidJson_Invalid) {
+  Config config;
+  EXPECT_FALSE(ParseConfig(kInvalidJson, &config));
+}
+
+TEST(ParseConfigTest, FullConfig_Valid) {
+  Config config;
+  EXPECT_TRUE(ParseConfig(kValidConfig, &config));
+  EXPECT_EQ(10000000, config.download_size);
+  EXPECT_EQ(20000000, config.upload_size);
+  EXPECT_EQ(20, config.num_downloads);
+  EXPECT_EQ(15, config.num_uploads);
+  EXPECT_EQ(200, config.interval_size);
+  EXPECT_EQ("Kansas City", config.location_name);
+  EXPECT_EQ(10, config.min_transfer_intervals);
+  EXPECT_EQ(25, config.max_transfer_intervals);
+  EXPECT_EQ(5000, config.min_transfer_runtime);
+  EXPECT_EQ(20000, config.max_transfer_runtime);
+  EXPECT_EQ(0.08, config.max_transfer_variance);
+  EXPECT_EQ(3000, config.ping_runtime);
+  EXPECT_EQ(300, config.ping_timeout);
+  EXPECT_EQ(3004, config.transfer_port_start);
+  EXPECT_EQ(3023, config.transfer_port_end);
+}
+
+TEST(ParseServersTest, NullServers_Invalid) {
+  EXPECT_FALSE(ParseServers(kValidServers, nullptr));
+}
+
+TEST(ParseServersTest, EmptyServers_Invalid) {
+  std::vector<http::Url> servers;
+  EXPECT_FALSE(ParseServers("", &servers));
+}
+
+TEST(ParseServersTest, InvalidJson_Invalid) {
+  std::vector<http::Url> servers;
+  EXPECT_FALSE(ParseServers(kInvalidJson, &servers));
+}
+
+TEST(ParseServersTest, FullServers_Valid) {
+  std::vector<http::Url> servers;
+  EXPECT_TRUE(ParseServers(kValidServers, &servers));
+  EXPECT_THAT(servers, testing::UnorderedElementsAre(
+      http::Url("http://austin.speed.googlefiber.net/"),
+      http::Url("http://kansas.speed.googlefiber.net/"),
+      http::Url("http://provo.speed.googlefiber.net/"),
+      http::Url("http://stanford.speed.googlefiber.net/")));
+}
+
+TEST(ParseServersTest, InvalidServers_Invalid) {
+  std::vector<http::Url> servers;
+  EXPECT_FALSE(ParseServers(kInvalidServers, &servers));
+}
+
+}  // namespace
+}  // namespace speedtest
diff --git a/speedtest/curl_env.cc b/speedtest/curl_env.cc
index c02835c..4a49b99 100644
--- a/speedtest/curl_env.cc
+++ b/speedtest/curl_env.cc
@@ -18,34 +18,106 @@
 
 #include <cstdlib>
 #include <iostream>
-#include <curl/curl.h>
 #include "errors.h"
 #include "request.h"
 
 namespace http {
+namespace {
 
-CurlEnv::CurlEnv() {
-  init(CURL_GLOBAL_NOTHING);
+void LockFn(CURL *handle,
+            curl_lock_data data,
+            curl_lock_access access,
+            void *userp) {
+  CurlEnv *env = static_cast<CurlEnv *>(userp);
+  env->Lock(data);
 }
 
-CurlEnv::CurlEnv(int init_options) {
-  init(init_options);
+void UnlockFn(CURL *handle, curl_lock_data data, void *userp) {
+  CurlEnv *env = static_cast<CurlEnv *>(userp);
+  env->Unlock(data);
 }
 
-CurlEnv::~CurlEnv() {
-  curl_global_cleanup();
+}  // namespace
+
+std::shared_ptr<CurlEnv> CurlEnv::NewCurlEnv(const Options &options) {
+  return std::shared_ptr<CurlEnv>(new CurlEnv(options));
 }
 
-std::unique_ptr<Request> CurlEnv::NewRequest() {
-  return std::unique_ptr<Request>(new Request(shared_from_this()));
-}
-
-void CurlEnv::init(int init_flags) {
-  CURLcode status = curl_global_init(init_flags);
+CurlEnv::CurlEnv(const Options &options)
+    : options_(options),
+      set_max_connections_(false),
+      share_(nullptr) {
+  CURLcode status;
+  {
+    std::lock_guard <std::mutex> lock(curl_mutex_);
+    status = curl_global_init(options_.curl_options);
+  }
   if (status != 0) {
     std::cerr << "Curl initialization failed: " << ErrorString(status);
     std::exit(1);
   }
+  if (!options_.disable_dns_cache) {
+    share_ = curl_share_init();
+    curl_share_setopt(share_, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
+    curl_share_setopt(share_, CURLSHOPT_USERDATA, this);
+    curl_share_setopt(share_, CURLSHOPT_LOCKFUNC, &LockFn);
+    curl_share_setopt(share_, CURLSHOPT_UNLOCKFUNC, &UnlockFn);
+  }
+}
+
+CurlEnv::~CurlEnv() {
+  curl_share_cleanup(share_);
+  share_ = nullptr;
+  curl_global_cleanup();
+}
+
+std::unique_ptr<Request> CurlEnv::NewRequest(const Url &url) {
+  // curl_global_init is not threadsafe and calling curl_easy_init may
+  // implicitly call it so we need to mutex lock on creating all requests
+  // to ensure the global initialization is done in a threadsafe manner.
+  std::lock_guard <std::mutex> lock(curl_mutex_);
+
+  // We use an aliasing constructor on a shared_ptr to keep a reference to
+  // CurlEnv as when the refcount drops to 0 we want to do global cleanup.
+  // So the CURL handle for this shared_ptr is _unmanaged_ and the Request
+  // object is responsible for cleaning it up, which involves calling
+  // curl_easy_cleanup().
+  //
+  // This way Request doesn't need to know about CurlEnv at all, while
+  // all Request instances will still keep an implicit reference to
+  // CurlEnv.
+  std::shared_ptr<CURL> handle(shared_from_this(), curl_easy_init());
+
+  // For some reason libcurl sets the max connections on a handle.
+  // According to the docs, doing so when there are open connections may
+  // close them so we maintain this boolean so as to set the maximum
+  // number of connections on the connection pool associated with this
+  // handle before any connections are opened.
+  if (!set_max_connections_ && options_.max_connections > 0) {
+    curl_easy_setopt(handle.get(),
+                     CURLOPT_MAXCONNECTS,
+                     options_.max_connections);
+    set_max_connections_ = true;
+  }
+
+  curl_easy_setopt(handle.get(), CURLOPT_SHARE, share_);
+  return std::unique_ptr<Request>(new Request(handle, url));
+}
+
+void CurlEnv::Lock(curl_lock_data lock_type) {
+  if (lock_type == CURL_LOCK_DATA_DNS) {
+    // It is ill-advised to call lock directly but libcurl uses
+    // separate lock/unlock functions.
+    dns_mutex_.lock();
+  }
+}
+
+void CurlEnv::Unlock(curl_lock_data lock_type) {
+  if (lock_type == CURL_LOCK_DATA_DNS) {
+    // It is ill-advised to call lock directly but libcurl uses
+    // separate lock/unlock functiounknns.
+    dns_mutex_.unlock();
+  }
 }
 
 }  // namespace http
diff --git a/speedtest/curl_env.h b/speedtest/curl_env.h
index 09511fa..85a34ba 100644
--- a/speedtest/curl_env.h
+++ b/speedtest/curl_env.h
@@ -17,27 +17,45 @@
 #ifndef HTTP_CURL_ENV_H
 #define HTTP_CURL_ENV_H
 
+#include <curl/curl.h>
 #include <memory>
+#include <mutex>
+#include "url.h"
 
 namespace http {
 
 class Request;
 
-// Curl initialization to cleanup automatically
 class CurlEnv : public std::enable_shared_from_this<CurlEnv> {
  public:
-  CurlEnv();
-  explicit CurlEnv(int init_options);
+  struct Options {
+    int curl_options = CURL_GLOBAL_NOTHING;
+    bool disable_dns_cache = false;
+    int max_connections = 0;
+  };
+
+  static std::shared_ptr<CurlEnv> NewCurlEnv(const Options &options);
   virtual ~CurlEnv();
 
-  std::unique_ptr<Request> NewRequest();
+  std::unique_ptr<Request> NewRequest(const Url &url);
+
+  void Lock(curl_lock_data lock_type);
+  void Unlock(curl_lock_data lock_type);
 
  private:
-  void init(int flags);
+  explicit CurlEnv(const Options &options);
+
+  Options options_;
+
+  // used to lock on curl global state
+  std::mutex curl_mutex_;
+  bool set_max_connections_;
+
+  std::mutex dns_mutex_;
+  CURLSH *share_;  // owned
 
   // disable
   CurlEnv(const CurlEnv &other) = delete;
-
   void operator=(const CurlEnv &other) = delete;
 };
 
diff --git a/speedtest/download_test.cc b/speedtest/download_test.cc
new file mode 100644
index 0000000..f23d97a
--- /dev/null
+++ b/speedtest/download_test.cc
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "download_test.h"
+
+#include <algorithm>
+#include <cassert>
+#include <iostream>
+#include <thread>
+#include "generic_test.h"
+#include "utils.h"
+
+namespace speedtest {
+
+DownloadTest::DownloadTest(const Options &options)
+    : TransferTest(options_),
+      options_(options) {
+  assert(options_.num_transfers > 0);
+  assert(options_.download_size > 0);
+}
+
+void DownloadTest::RunInternal() {
+  ResetCounters();
+  threads_.clear();
+  if (options_.verbose) {
+    std::cout << "Downloading " << options_.num_transfers
+              << " threads with " << options_.download_size << " bytes\n";
+  }
+  for (int i = 0; i < options_.num_transfers; ++i) {
+    threads_.emplace_back([=]{
+      RunDownload(i);
+    });
+  }
+}
+
+void DownloadTest::StopInternal() {
+  std::for_each(threads_.begin(), threads_.end(), [](std::thread &t) {
+    t.join();
+  });
+}
+
+void DownloadTest::RunDownload(int id) {
+  GenericTest::RequestPtr download = options_.request_factory(id);
+  while (GetStatus() == TestStatus::RUNNING) {
+    long downloaded = 0;
+    download->set_param("i", to_string(id));
+    download->set_param("size", to_string(options_.download_size));
+    download->set_param("time", to_string(SystemTimeMicros()));
+    download->set_progress_fn([&](curl_off_t,
+                                  curl_off_t dlnow,
+                                  curl_off_t,
+                                  curl_off_t) -> bool {
+      if (dlnow > downloaded) {
+        TransferBytes(dlnow - downloaded);
+        downloaded = dlnow;
+      }
+      return GetStatus() != TestStatus::RUNNING;
+    });
+    StartRequest();
+    download->Get();
+    EndRequest();
+    download->Reset();
+  }
+}
+
+}  // namespace speedtest
diff --git a/speedtest/download_test.h b/speedtest/download_test.h
new file mode 100644
index 0000000..f8a249f
--- /dev/null
+++ b/speedtest/download_test.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SPEEDTEST_DOWNLOAD_TEST_H
+#define SPEEDTEST_DOWNLOAD_TEST_H
+
+#include <thread>
+#include <vector>
+#include "transfer_test.h"
+
+namespace speedtest {
+
+class DownloadTest : public TransferTest {
+ public:
+  struct Options : TransferTest::Options {
+    int download_size = 0;
+  };
+
+  explicit DownloadTest(const Options &options);
+
+ protected:
+  void RunInternal() override;
+  void StopInternal() override;
+
+ private:
+  void RunDownload(int id);
+
+  Options options_;
+  std::vector<std::thread> threads_;
+
+  // disallowed
+  DownloadTest(const DownloadTest &) = delete;
+  void operator=(const DownloadTest &) = delete;
+};
+
+}  // namespace speedtest
+
+#endif  // SPEEDTEST_DOWNLOAD_TEST_H
diff --git a/speedtest/generic_test.cc b/speedtest/generic_test.cc
new file mode 100644
index 0000000..be012ba
--- /dev/null
+++ b/speedtest/generic_test.cc
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "generic_test.h"
+
+#include <cassert>
+#include "utils.h"
+
+namespace speedtest {
+
+const char *AsString(TestStatus status) {
+  switch (status) {
+    case TestStatus::NOT_STARTED: return "NOT_STARTED";
+    case TestStatus::RUNNING: return "RUNNING";
+    case TestStatus::STOPPING: return "STOPPING";
+    case TestStatus::STOPPED: return "STOPPED";
+  }
+  std::exit(1);
+}
+
+GenericTest::GenericTest(const Options &options)
+    : status_(TestStatus::NOT_STARTED) {
+  assert(options.request_factory);
+}
+
+void GenericTest::Run() {
+  {
+    std::lock_guard <std::mutex> lock(mutex_);
+    if (status_ != TestStatus::NOT_STARTED &&
+        status_ != TestStatus::STOPPED) {
+      return;
+    }
+    status_ = TestStatus::RUNNING;
+    start_time_ = SystemTimeMicros();
+  }
+  RunInternal();
+}
+
+void GenericTest::Stop() {
+  {
+    std::lock_guard <std::mutex> lock(mutex_);
+    if (status_ != TestStatus::RUNNING) {
+      return;
+    }
+    status_ = TestStatus::STOPPING;
+  }
+  StopInternal();
+  std::lock_guard <std::mutex> lock(mutex_);
+  status_ = TestStatus::STOPPED;
+  end_time_ = SystemTimeMicros();
+}
+
+TestStatus GenericTest::GetStatus() const {
+  std::lock_guard <std::mutex> lock(mutex_);
+  return status_;
+}
+
+long GenericTest::GetRunningTime() const {
+  std::lock_guard <std::mutex> lock(mutex_);
+  switch (status_) {
+    case TestStatus::NOT_STARTED:
+      break;
+    case TestStatus::RUNNING:
+    case TestStatus::STOPPING:
+      return SystemTimeMicros() - start_time_;
+    case TestStatus::STOPPED:
+      return end_time_ - start_time_;
+  }
+  return 0;
+}
+
+void GenericTest::WaitForEnd() {
+  std::unique_lock<std::mutex> lock(mutex_);
+  condition_.wait(lock, [this]{
+    return status_ == TestStatus::STOPPED;
+  });
+}
+
+}  // namespace speedtest
diff --git a/speedtest/generic_test.h b/speedtest/generic_test.h
new file mode 100644
index 0000000..cf0d37c
--- /dev/null
+++ b/speedtest/generic_test.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SPEEDTEST_GENERIC_TEST_H
+#define SPEEDTEST_GENERIC_TEST_H
+
+#include <condition_variable>
+#include <functional>
+#include <memory>
+#include <mutex>
+#include "request.h"
+
+namespace speedtest {
+
+enum class TestStatus {
+  NOT_STARTED,
+  RUNNING,
+  STOPPING,
+  STOPPED
+};
+
+const char *AsString(TestStatus status);
+
+class GenericTest {
+ public:
+  using RequestPtr = std::unique_ptr<http::Request>;
+
+  struct Options {
+    bool verbose = false;
+    std::function<RequestPtr(int)> request_factory;
+  };
+
+  explicit GenericTest(const Options &options);
+
+  void Run();
+  void Stop();
+
+  TestStatus GetStatus() const;
+  long GetRunningTime() const;
+  void WaitForEnd();
+
+ protected:
+  virtual void RunInternal() = 0;
+  virtual void StopInternal() {}
+
+ private:
+  mutable std::mutex mutex_;
+  std::condition_variable condition_;
+  TestStatus status_;
+  bool running_ = false;
+  long start_time_;
+  long end_time_;
+
+  // disallowed
+  GenericTest(const GenericTest &) = delete;
+
+  void operator=(const GenericTest &) = delete;
+};
+
+}  // namespace speedtest
+
+#endif  //SPEEDTEST_GENERIC_TEST_H
diff --git a/speedtest/options.cc b/speedtest/options.cc
index 4d07099..db7f3ae 100644
--- a/speedtest/options.cc
+++ b/speedtest/options.cc
@@ -25,7 +25,7 @@
 
 namespace speedtest {
 
-const char* kDefaultHost = "speedtest.googlefiber.net";
+const char* kDefaultHost = "any.speed.gfsvc.com";
 
 namespace {
 
@@ -58,67 +58,98 @@
   return true;
 }
 
-const char *kShortOpts = "d:u:t:n:p:s:vh";
+const int kOptMinTransferTime = 1000;
+const int kOptMaxTransferTime = 1001;
+const int kOptPingRuntime = 1002;
+const int kOptPingTimeout = 1003;
+const int kOptDisableDnsCache = 1004;
+const int kOptMaxConnections = 1005;
+const char *kShortOpts = "hvg:a:d:s:t:u:p:";
 struct option kLongOpts[] = {
     {"help", no_argument, nullptr, 'h'},
-    {"number", required_argument, nullptr, 'n'},
-    {"download_size", required_argument, nullptr, 'd'},
-    {"upload_size", required_argument, nullptr, 'u'},
-    {"progress", required_argument, nullptr, 'p'},
-    {"serverid", required_argument, nullptr, 's'},  // ignored
-    {"time", required_argument, nullptr, 't'},
     {"verbose", no_argument, nullptr, 'v'},
+    {"global_host", required_argument, nullptr, 'g'},
+    {"user_agent", required_argument, nullptr, 'a'},
+    {"num_downloads", required_argument, nullptr, 'd'},
+    {"download_size", required_argument, nullptr, 's'},
+    {"num_uploads", required_argument, nullptr, 'u'},
+    {"upload_size", required_argument, nullptr, 't'},
+    {"progress", required_argument, nullptr, 'p'},
+    {"min_transfer_time", required_argument, nullptr, kOptMinTransferTime},
+    {"max_transfer_time", required_argument, nullptr, kOptMaxTransferTime},
+    {"ping_runtime", required_argument, nullptr, kOptPingRuntime},
+    {"ping_timeout", required_argument, nullptr, kOptPingTimeout},
+    {"disable_dns_cache", no_argument, nullptr, kOptDisableDnsCache},
+    {"max_connections", required_argument, nullptr, kOptMaxConnections},
     {nullptr, 0, nullptr, 0},
 };
-const int kDefaultNumber = 10;
-const int kDefaultDownloadSize = 10000000;
-const int kDefaultUploadSize = 10000000;
-const int kDefaultTimeMillis = 5000;
 const int kMaxNumber = 1000;
 const int kMaxProgress = 1000000;
 
+const char *kSpeedtestHelp = R"USAGE(: run an HTTP speedtest.
+
+If no hosts are specified, the global host is queried for a list
+of servers to use, otherwise the list of supplied hosts will be
+used. Each will be pinged several times and the one with the
+lowest ping time will be used. If only one host is supplied, it
+will be used without pinging.
+
+Usage: speedtest [options] [host ...]
+ -h, --help                This help text
+ -v, --verbose             Verbose output
+ -g, --global_host URL     Global host URL
+ -a, --user_agent AGENT    User agent string for HTTP requests
+ --disable_dns_cache       Disable global DNS cache
+ --max_connections NUM     Maximum number of parallel connections
+
+These options override the speedtest config parameters:
+ -d, --num_downloads NUM   Number of simultaneous downloads
+ -p, --progress TIME       Progress intervals in milliseconds
+ -s, --download_size SIZE  Download size in bytes
+ -t, --upload_size SIZE    Upload size in bytes
+ -u, --num_uploads NUM     Number of simultaneous uploads
+ --min_transfer_time TIME  Minimum transfer time in milliseconds
+ --max_transfer_time TIME  Maximum transfer time in milliseconds
+ --ping_time TIME          Ping time in milliseconds
+ --ping_timeout TIME       Ping timeout in milliseconds;
+)USAGE";
+
 }  // namespace
 
 bool ParseOptions(int argc, char *argv[], Options *options) {
   assert(options != nullptr);
-  options->number = kDefaultNumber;
-  options->download_size = kDefaultDownloadSize;
-  options->upload_size = kDefaultUploadSize;
-  options->time_millis = kDefaultTimeMillis;
-  options->progress_millis = 0;
-  options->verbose = false;
   options->usage = false;
+  options->verbose = false;
+  options->global_host = http::Url(kDefaultHost);
+  options->global = false;
+  options->disable_dns_cache = false;
+  options->max_connections = 0;
+  options->num_downloads = 0;
+  options->download_size = 0;
+  options->num_uploads = 0;
+  options->upload_size = 0;
+  options->progress_millis = 0;
+  options->min_transfer_time = 0;
+  options->max_transfer_time = 0;
+  options->ping_runtime = 0;
+  options->ping_timeout = 0;
   options->hosts.clear();
 
+  if (!options->global_host.ok()) {
+    std::cerr << "Invalid global host " << kDefaultHost << "\n";
+    return false;
+  }
+
   // Manually set this to 0 to allow repeated calls
   optind = 0;
-
   int opt = 0, long_index = 0;
   while ((opt = getopt_long(argc, argv,
                             kShortOpts, kLongOpts, &long_index)) != -1) {
     switch (opt) {
-      case 'd':
-        if (!ParseSize(optarg, &options->download_size)) {
-          std::cerr << "Invalid download size '" << optarg << "'\n";
-          return false;
-        }
+      case 'a':
+        options->user_agent = optarg;
         break;
-      case 'u':
-        if (!ParseSize(optarg, &options->upload_size)) {
-          std::cerr << "Invalid upload size '" << optarg << "'\n";
-          return false;
-        }
-        break;
-      case 't':
-        options->time_millis = atoi(optarg);
-        break;
-      case 'v':
-        options->verbose = true;
-        break;
-      case 'h':
-        options->usage = true;
-        return true;
-      case 'n': {
+      case 'd': {
         long number;
         char *endptr;
         if (!ParseLong(optarg, &endptr, &number)) {
@@ -127,12 +158,24 @@
         }
         if (number < 1 || number > kMaxNumber) {
           std::cerr << "Number must be between 1 and " << kMaxNumber
-          << ", got '" << optarg << "'\n";
+                    << ", got '" << optarg << "'\n";
           return false;
         }
-        options->number = static_cast<int>(number);
+        options->num_downloads = static_cast<int>(number);
         break;
       }
+      case 'g': {
+        http::Url url(optarg);
+        if (!url.ok()) {
+          std::cerr << "Invalid global host " << optarg << "\n";
+          return false;
+        }
+        options->global_host = url;
+        break;
+      }
+      case 'h':
+        options->usage = true;
+        return true;
       case 'p': {
         long progress;
         char *endptr;
@@ -142,16 +185,122 @@
         }
         if (progress < 0 || progress > kMaxProgress) {
           std::cerr << "Number must be between 0 and " << kMaxProgress
-          << ", got '" << optarg << "'\n";
+                    << ", got '" << optarg << "'\n";
           return false;
         }
         options->progress_millis = static_cast<int>(progress);
         break;
       }
       case 's':
-        // serverid is an argument supported by the older speedtest
-        // implementation. It is ignored here to ease the transition.
+        if (!ParseSize(optarg, &options->download_size)) {
+          std::cerr << "Invalid download size '" << optarg << "'\n";
+          return false;
+        }
         break;
+      case 't':
+        if (!ParseSize(optarg, &options->upload_size)) {
+          std::cerr << "Invalid upload size '" << optarg << "'\n";
+          return false;
+        }
+        break;
+      case 'u': {
+        long number;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &number)) {
+          std::cerr << "Could not parse number '" << optarg << "'\n";
+          return false;
+        }
+        if (number < 1 || number > kMaxNumber) {
+          std::cerr << "Number must be between 1 and " << kMaxNumber
+                    << ", got '" << optarg << "'\n";
+          return false;
+        }
+        options->num_uploads = static_cast<int>(number);
+        break;
+      }
+      case 'v':
+        options->verbose = true;
+        break;
+      case kOptMinTransferTime: {
+        long transfer_time;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &transfer_time)) {
+          std::cerr << "Could not parse minimum transfer time '"
+                    << optarg << "'\n";
+          return false;
+        }
+        if (transfer_time < 0) {
+          std::cerr << "Minimum transfer time must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->min_transfer_time = static_cast<int>(transfer_time);
+        break;
+      }
+      case kOptMaxTransferTime: {
+        long transfer_time;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &transfer_time)) {
+          std::cerr << "Could not parse maximum transfer time '"
+                    << optarg << "'\n";
+          return false;
+        }
+        if (transfer_time < 0) {
+          std::cerr << "Maximum transfer must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->max_transfer_time = static_cast<int>(transfer_time);
+        break;
+      }
+      case kOptPingRuntime: {
+        long ping_runtime;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &ping_runtime)) {
+          std::cerr << "Could not parse ping time '" << optarg << "'\n";
+          return false;
+        }
+        if (ping_runtime < 0) {
+          std::cerr << "Ping time must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->ping_runtime = static_cast<int>(ping_runtime);
+        break;
+      }
+      case kOptPingTimeout: {
+        long ping_timeout;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &ping_timeout)) {
+          std::cerr << "Could not parse ping timeout '" << optarg << "'\n";
+          return false;
+        }
+        if (ping_timeout < 0) {
+          std::cerr << "Ping timeout must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->ping_timeout = static_cast<int>(ping_timeout);
+        break;
+      }
+      case kOptDisableDnsCache:
+        options->disable_dns_cache = true;
+        break;
+      case kOptMaxConnections: {
+        long max_connections;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &max_connections)) {
+          std::cerr << "Could not parse max connections '" << optarg << "'\n";
+          return false;
+        }
+        if (max_connections < 0) {
+          std::cerr << "Max connections must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->max_connections = static_cast<int>(max_connections);
+        break;
+      }
       default:
         return false;
     }
@@ -174,7 +323,7 @@
     }
   }
   if (options->hosts.empty()) {
-    options->hosts.emplace_back(http::Url(kDefaultHost));
+    options->global = true;
   }
   return true;
 }
@@ -184,13 +333,23 @@
 }
 
 void PrintOptions(std::ostream &out, const Options &options) {
-  out << "Number: " << options.number << "\n"
-      << "Upload size: " << options.upload_size << "\n"
-      << "Download size: " << options.download_size << "\n"
-      << "Time: " << options.time_millis << " ms\n"
-      << "Progress interval: " << options.progress_millis << " ms\n"
+  out << "Usage: " << (options.usage ? "true" : "false") << "\n"
       << "Verbose: " << (options.verbose ? "true" : "false") << "\n"
-      << "Usage: " << (options.usage ? "true" : "false") << "\n"
+      << "Global host: " << options.global_host.url() << "\n"
+      << "Global: " << (options.global ? "true" : "false") << "\n"
+      << "User agent: " << options.user_agent << "\n"
+      << "Disable DNS cache: "
+      << (options.disable_dns_cache ? "true" : "false") << "\n"
+      << "Max connections: " << options.max_connections << "\n"
+      << "Number of downloads: " << options.num_downloads << "\n"
+      << "Download size: " << options.download_size << " bytes\n"
+      << "Number of uploads: " << options.num_uploads << "\n"
+      << "Upload size: " << options.upload_size << " bytes\n"
+      << "Min transfer time: " << options.min_transfer_time << " ms\n"
+      << "Max transfer time: " << options.max_transfer_time << " ms\n"
+      << "Ping runtime: " << options.ping_runtime << " ms\n"
+      << "Ping timeout: " << options.ping_timeout << " ms\n"
+      << "Progress interval: " << options.progress_millis << " ms\n"
       << "Hosts:\n";
   for (const http::Url &host : options.hosts) {
     out << "  " << host.url() << "\n";
@@ -205,15 +364,7 @@
   assert(app_path != nullptr);
   const char *last_slash = strrchr(app_path, '/');
   const char *app_name = last_slash == nullptr ? app_path : last_slash + 1;
-  out << basename(app_name) << ": run an HTTP speedtest\n\n"
-      << "Usage: speedtest [options] <host> ...\n"
-      << " -h, --help                This help text\n"
-      << " -n, --number NUM          Number of simultaneous transfers\n"
-      << " -d, --download_size SIZE  Download size in bytes\n"
-      << " -u, --upload_size SIZE    Upload size in bytes\n"
-      << " -t, --time TIME           Time per test in milliseconds\n"
-      << " -p, --progress TIME       Progress intervals in milliseconds\n"
-      << " -v, --verbose             Verbose output\n";
+  out << basename(app_name) << kSpeedtestHelp;
 }
 
 }  // namespace speedtest
diff --git a/speedtest/options.h b/speedtest/options.h
index 0996798..0413798 100644
--- a/speedtest/options.h
+++ b/speedtest/options.h
@@ -27,14 +27,26 @@
 extern const char* kDefaultHost;
 
 struct Options {
-  std::vector<http::Url> hosts;
-  int number;
-  long download_size;
-  long upload_size;
-  int time_millis;
-  int progress_millis;
   bool verbose;
   bool usage;
+  http::Url global_host;
+  bool global;
+  std::string user_agent;
+  bool disable_dns_cache;
+  int max_connections;
+
+  // A value of 0 means use the speedtest config parameters
+  int num_downloads;
+  long download_size;
+  int num_uploads;
+  long upload_size;
+  int progress_millis;
+  int min_transfer_time;
+  int max_transfer_time;
+  int ping_runtime;
+  int ping_timeout;
+
+  std::vector<http::Url> hosts;
 };
 
 // Parse command line options putting results into 'options'
diff --git a/speedtest/options_test.cc b/speedtest/options_test.cc
index 2754552..113f730 100644
--- a/speedtest/options_test.cc
+++ b/speedtest/options_test.cc
@@ -14,12 +14,14 @@
  * limitations under the License.
  */
 
+#include "options.h"
+
 #include <gtest/gtest.h>
 #include <gmock/gmock.h>
 #include <mutex>
 #include <string.h>
 #include <vector>
-#include "options.h"
+#include "url.h"
 
 namespace speedtest {
 namespace {
@@ -81,8 +83,22 @@
 TEST(OptionsTest, Empty_ValidDefault) {
   Options options;
   TestValidOptions({}, &options);
+  EXPECT_TRUE(options.global);
+  EXPECT_EQ(http::Url("any.speed.gfsvc.com"), options.global_host);
   EXPECT_FALSE(options.verbose);
   EXPECT_FALSE(options.usage);
+  EXPECT_FALSE(options.disable_dns_cache);
+  EXPECT_EQ(0, options.max_connections);
+  EXPECT_EQ(0, options.num_downloads);
+  EXPECT_EQ(0, options.download_size);
+  EXPECT_EQ(0, options.num_uploads);
+  EXPECT_EQ(0, options.upload_size);
+  EXPECT_EQ(0, options.progress_millis);
+  EXPECT_EQ(0, options.min_transfer_time);
+  EXPECT_EQ(0, options.max_transfer_time);
+  EXPECT_EQ(0, options.ping_runtime);
+  EXPECT_EQ(0, options.ping_timeout);
+  EXPECT_THAT(options.hosts, testing::IsEmpty());
 }
 
 TEST(OptionsTest, Usage_Valid) {
@@ -104,35 +120,68 @@
   EXPECT_THAT(options.hosts, testing::ElementsAre(http::Url("efgh")));
 }
 
-TEST(OptionsTest, FullShort_Valid) {
+TEST(OptionsTest, ShortOptions_Valid) {
   Options options;
-  TestValidOptions({"-d", "5122",
-                    "-u", "7653",
-                    "-t", "123",
-                    "-n", "15",
-                    "-p", "500"},
+  TestValidOptions({"-s", "5122",
+                    "-t", "7653",
+                    "-d", "20",
+                    "-u", "15",
+                    "-p", "500",
+                    "-g", "speed.gfsvc.com",
+                    "-a", "CrOS",
+                    "foo.speed.googlefiber.net",
+                    "bar.speed.googlefiber.net"},
                     &options);
   EXPECT_EQ(5122, options.download_size);
   EXPECT_EQ(7653, options.upload_size);
-  EXPECT_EQ(123, options.time_millis);
-  EXPECT_EQ(15, options.number);
+  EXPECT_EQ(20, options.num_downloads);
+  EXPECT_EQ(15, options.num_uploads);
   EXPECT_EQ(500, options.progress_millis);
+  EXPECT_EQ(http::Url("speed.gfsvc.com"), options.global_host);
+  EXPECT_EQ("CrOS", options.user_agent);
+  EXPECT_EQ(0, options.min_transfer_time);
+  EXPECT_EQ(0, options.max_transfer_time);
+  EXPECT_EQ(0, options.ping_runtime);
+  EXPECT_EQ(0, options.ping_timeout);
+  EXPECT_THAT(options.hosts, testing::UnorderedElementsAre(
+      http::Url("foo.speed.googlefiber.net"),
+      http::Url("bar.speed.googlefiber.net")));
 }
 
-TEST(OptionsTest, FullLong_Valid) {
+TEST(OptionsTest, LongOptions_Valid) {
   Options options;
   TestValidOptions({"--download_size", "5122",
                     "--upload_size", "7653",
-                    "--time", "123",
                     "--progress", "1000",
-                    "--number", "12"},
+                    "--num_uploads", "12",
+                    "--num_downloads", "16",
+                    "--global_host", "speed.gfsvc.com",
+                    "--user_agent", "CrOS",
+                    "--disable_dns_cache",
+                    "--max_connections", "23",
+                    "--min_transfer_time", "7500",
+                    "--max_transfer_time", "13500",
+                    "--ping_runtime", "2500",
+                    "--ping_timeout", "300",
+                    "foo.speed.googlefiber.net",
+                    "bar.speed.googlefiber.net"},
                     &options);
+  EXPECT_TRUE(options.disable_dns_cache);
   EXPECT_EQ(5122, options.download_size);
   EXPECT_EQ(7653, options.upload_size);
-  EXPECT_EQ(123, options.time_millis);
-  EXPECT_EQ(12, options.number);
+  EXPECT_EQ(16, options.num_downloads);
+  EXPECT_EQ(23, options.max_connections);
+  EXPECT_EQ(12, options.num_uploads);
   EXPECT_EQ(1000, options.progress_millis);
-  EXPECT_THAT(options.hosts, testing::ElementsAre(http::Url(kDefaultHost)));
+  EXPECT_EQ(http::Url("speed.gfsvc.com"), options.global_host);
+  EXPECT_EQ("CrOS", options.user_agent);
+  EXPECT_EQ(7500, options.min_transfer_time);
+  EXPECT_EQ(13500, options.max_transfer_time);
+  EXPECT_EQ(2500, options.ping_runtime);
+  EXPECT_EQ(300, options.ping_timeout);
+  EXPECT_THAT(options.hosts, testing::UnorderedElementsAre(
+      http::Url("foo.speed.googlefiber.net"),
+      http::Url("bar.speed.googlefiber.net")));
 }
 
 }  // namespace
diff --git a/speedtest/ping_test.cc b/speedtest/ping_test.cc
new file mode 100644
index 0000000..999b53d
--- /dev/null
+++ b/speedtest/ping_test.cc
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ping_test.h"
+
+#include <algorithm>
+#include <cassert>
+#include <iomanip>
+#include <iostream>
+#include "generic_test.h"
+#include "utils.h"
+
+namespace speedtest {
+
+PingTest::PingTest(const Options &options)
+    : GenericTest(options),
+      options_(options) {
+  assert(options_.num_pings > 0);
+}
+
+void PingTest::RunInternal() {
+  ResetCounters();
+  success_ = false;
+  threads_.clear();
+  for (int i = 0; i < options_.num_pings; ++i) {
+    threads_.emplace_back([=]() {
+      RunPing(i);
+    });
+  }
+}
+
+void PingTest::StopInternal() {
+  std::for_each(threads_.begin(), threads_.end(), [](std::thread &t) {
+    t.join();
+  });
+  threads_.clear();
+  if (options_.verbose) {
+    std::cout << "Pinged " << options_.num_pings << " "
+              << (options_.num_pings == 1 ? "host" : "hosts") << ":\n";
+  }
+  const PingStats *min_stats = nullptr;
+  for (const auto &stat : stats_) {
+    if (options_.verbose) {
+      std::cout << "  " << stat.url.url() << ": ";
+      if (stat.pings_received == 0) {
+        std::cout << "no packets received";
+      } else {
+        double mean_micros = ((double) stat.total_micros) / stat.pings_received;
+        std::cout << "min " << round(stat.min_micros / 1000.0d, 2) << " ms"
+                  << " from " << stat.pings_received << " pings"
+                  << " (mean " << round(mean_micros / 1000.0d, 2) << " ms)";
+      }
+      std::cout << "\n";
+    }
+    if (stat.pings_received > 0) {
+      if (!min_stats || stat.min_micros < min_stats->min_micros) {
+        min_stats = &stat;
+      }
+    }
+  }
+
+  std::lock_guard<std::mutex> lock(mutex_);
+  if (!min_stats) {
+    // no servers respondeded
+    success_ = false;
+    return;
+  } else {
+    fastest_ = *min_stats;
+    success_ = true;
+  }
+}
+
+void PingTest::RunPing(size_t index) {
+  GenericTest::RequestPtr ping = options_.request_factory(index);
+  stats_[index].url = ping->url();
+  while (GetStatus() == TestStatus::RUNNING) {
+    long req_start = SystemTimeMicros();
+    if (ping->Get() == CURLE_OK) {
+      long req_end = SystemTimeMicros();
+      long ping_time = req_end - req_start;
+      stats_[index].total_micros += ping_time;
+      stats_[index].pings_received++;
+      stats_[index].min_micros = std::min(stats_[index].min_micros, ping_time);
+    }
+    ping->Reset();
+    std::this_thread::sleep_for(std::chrono::milliseconds(100));
+  }
+}
+
+bool PingTest::IsSucceeded() const {
+  return success_;
+}
+
+PingStats PingTest::GetFastest() const {
+  std::lock_guard<std::mutex> lock(mutex_);
+  return fastest_;
+}
+
+void PingTest::ResetCounters() {
+  stats_.clear();
+  stats_.resize(options_.num_pings);
+}
+
+}  // namespace speedtest
diff --git a/speedtest/ping_test.h b/speedtest/ping_test.h
new file mode 100644
index 0000000..558c799
--- /dev/null
+++ b/speedtest/ping_test.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef PING_TEST_H
+#define PING_TEST_H
+
+#include <atomic>
+#include <functional>
+#include <limits>
+#include <memory>
+#include <mutex>
+#include <thread>
+#include <vector>
+#include "generic_test.h"
+#include "request.h"
+#include "url.h"
+
+namespace speedtest {
+
+struct PingStats {
+  long total_micros = 0;
+  int pings_received = 0;
+  long min_micros = std::numeric_limits<long>::max();
+  http::Url url;
+};
+
+class PingTest : public GenericTest {
+ public:
+  struct Options : GenericTest::Options {
+    int timeout = 0;
+    int num_pings = 0;
+    std::function<RequestPtr(int)> request_factory;
+  };
+
+  explicit PingTest(const Options &options);
+
+  bool IsSucceeded() const;
+
+  PingStats GetFastest() const;
+
+ protected:
+  void RunInternal() override;
+  void StopInternal() override;
+
+ private:
+  void RunPing(size_t index);
+
+  void ResetCounters();
+
+  Options options_;
+  std::vector<PingStats> stats_;
+  std::vector<std::thread> threads_;
+  std::atomic_bool success_;
+
+  mutable std::mutex mutex_;
+  PingStats fastest_;
+
+  // disallowed
+  PingTest(const PingTest &) = delete;
+  void operator=(const PingTest &) = delete;
+};
+
+}  // namespace speedtest
+
+#endif  // PING_TEST_H
diff --git a/speedtest/request.cc b/speedtest/request.cc
index b0a2b89..1542053 100644
--- a/speedtest/request.cc
+++ b/speedtest/request.cc
@@ -64,17 +64,14 @@
 
 const int kDefaultQueryStringSize = 200;
 
+const Request::DownloadFn noop = [](void *, size_t) { };
+
 }  // namespace
 
-Request::Request(std::shared_ptr<CurlEnv> env)
-    : curl_headers_(nullptr),
-      env_(env) {
-  assert(env_);
-  handle_ = curl_easy_init();
-  if (!handle_) {
-    std::cerr << "Failed to create handle\n";
-    std::exit(1);
-  }
+Request::Request(std::shared_ptr<CURL> handle, const Url &url)
+    : handle_(handle),
+      curl_headers_(nullptr),
+      url_(url) {
 }
 
 Request::~Request() {
@@ -82,35 +79,40 @@
     curl_slist_free_all(curl_headers_);
     curl_headers_ = nullptr;
   }
-  curl_easy_cleanup(handle_);
+
+  curl_easy_cleanup(handle_.get());
+}
+
+CURLcode Request::Get() {
+  return Get(noop);
 }
 
 CURLcode Request::Get(DownloadFn download_fn) {
   CommonSetup();
   if (download_fn) {
-    curl_easy_setopt(handle_, CURLOPT_WRITEFUNCTION, &WriteCallback);
-    curl_easy_setopt(handle_, CURLOPT_WRITEDATA, &download_fn);
+    curl_easy_setopt(handle_.get(), CURLOPT_WRITEFUNCTION, &WriteCallback);
+    curl_easy_setopt(handle_.get(), CURLOPT_WRITEDATA, &download_fn);
   }
   return Execute();
 }
 
 CURLcode Request::Post(UploadFn upload_fn) {
   CommonSetup();
-  curl_easy_setopt(handle_, CURLOPT_UPLOAD, 1);
-  curl_easy_setopt(handle_, CURLOPT_READFUNCTION, &ReadCallback);
-  curl_easy_setopt(handle_, CURLOPT_READDATA, &upload_fn);
+  curl_easy_setopt(handle_.get(), CURLOPT_UPLOAD, 1);
+  curl_easy_setopt(handle_.get(), CURLOPT_READFUNCTION, &ReadCallback);
+  curl_easy_setopt(handle_.get(), CURLOPT_READDATA, &upload_fn);
   return Execute();
 }
 
 CURLcode Request::Post(const char *data, curl_off_t data_len) {
   CommonSetup();
-  curl_easy_setopt(handle_, CURLOPT_POSTFIELDSIZE_LARGE, data_len);
-  curl_easy_setopt(handle_, CURLOPT_POSTFIELDS, data);
+  curl_easy_setopt(handle_.get(), CURLOPT_POSTFIELDSIZE_LARGE, data_len);
+  curl_easy_setopt(handle_.get(), CURLOPT_POSTFIELDS, data);
   return Execute();
 }
 
 void Request::Reset() {
-  curl_easy_reset(handle_);
+  curl_easy_reset(handle_.get());
   clear_progress_fn();
   clear_headers();
   clear_params();
@@ -156,6 +158,10 @@
   params_.clear();
 }
 
+void Request::set_timeout_millis(long millis) {
+  curl_easy_setopt(handle_.get(), CURLOPT_TIMEOUT_MS, millis);
+}
+
 void Request::UpdateUrl() {
   std::string query_string;
   query_string.reserve(kDefaultQueryStringSize);
@@ -165,10 +171,10 @@
     if (!query_string.empty()) {
       query_string.append("&");
     }
-    char *name = curl_easy_escape(handle_,
+    char *name = curl_easy_escape(handle_.get(),
                                   iter->first.data(),
                                   iter->first.length());
-    char *value = curl_easy_escape(handle_,
+    char *value = curl_easy_escape(handle_.get(),
                                    iter->second.data(),
                                    iter->second.length());
     query_string.append(name);
@@ -183,12 +189,14 @@
 void Request::CommonSetup() {
   UpdateUrl();
   std::string request_url = url_.url();
-  curl_easy_setopt(handle_, CURLOPT_URL, request_url.c_str());
-  curl_easy_setopt(handle_, CURLOPT_USERAGENT, user_agent_.c_str());
+  curl_easy_setopt(handle_.get(), CURLOPT_URL, request_url.c_str());
+  curl_easy_setopt(handle_.get(), CURLOPT_USERAGENT, user_agent_.c_str());
   if (progress_fn_) {
-    curl_easy_setopt(handle_, CURLOPT_NOPROGRESS, 0);
-    curl_easy_setopt(handle_, CURLOPT_XFERINFOFUNCTION, &ProgressCallback);
-    curl_easy_setopt(handle_, CURLOPT_XFERINFODATA, &progress_fn_);
+    curl_easy_setopt(handle_.get(), CURLOPT_NOPROGRESS, 0);
+    curl_easy_setopt(handle_.get(),
+                     CURLOPT_XFERINFOFUNCTION,
+                     &ProgressCallback);
+    curl_easy_setopt(handle_.get(), CURLOPT_XFERINFODATA, &progress_fn_);
   }
   if (!headers_.empty()) {
     struct curl_slist *headers = nullptr;
@@ -200,12 +208,12 @@
       header.append(iter->second);
       headers = curl_slist_append(headers, header.c_str());
     }
-    curl_easy_setopt(handle_, CURLOPT_HTTPHEADER, headers);
+    curl_easy_setopt(handle_.get(), CURLOPT_HTTPHEADER, headers);
   }
 }
 
 CURLcode Request::Execute() {
-  return curl_easy_perform(handle_);
+  return curl_easy_perform(handle_.get());
 }
 
 }  // namespace http
diff --git a/speedtest/request.h b/speedtest/request.h
index 837eecc..f3344d2 100644
--- a/speedtest/request.h
+++ b/speedtest/request.h
@@ -22,8 +22,6 @@
 #include <map>
 #include <memory>
 #include <string>
-
-#include "curl_env.h"
 #include "url.h"
 
 namespace http {
@@ -45,9 +43,10 @@
                                         curl_off_t,
                                         curl_off_t)>;
 
-  explicit Request(std::shared_ptr<CurlEnv> env);
+  Request(std::shared_ptr<CURL> handle, const Url &url);
   virtual ~Request();
 
+  CURLcode Get();
   CURLcode Get(DownloadFn download_fn);
   CURLcode Post(UploadFn upload_fn);
   CURLcode Post(const char *data, curl_off_t data_len);
@@ -80,24 +79,25 @@
   void set_progress_fn(ProgressFn progress_fn) { progress_fn_ = progress_fn; }
   void clear_progress_fn() { progress_fn_ = nullptr; }
 
+  // Request timeout
+  void set_timeout_millis(long millis);
+
   void UpdateUrl();
 
  private:
   void CommonSetup();
+
   CURLcode Execute();
 
   // owned
-  CURL *handle_;
+  std::shared_ptr<CURL> handle_;
   struct curl_slist *curl_headers_;
-
-  // ref-count CURL global config
-  std::shared_ptr<CurlEnv> env_;
   Url url_;
 
   std::string user_agent_;
   Headers headers_;
   QueryStringParams params_;
-  ProgressFn progress_fn_;  // unowned
+  ProgressFn progress_fn_;
 
   // disable
   Request(const Request &) = delete;
diff --git a/speedtest/request_test.cc b/speedtest/request_test.cc
index d8b11e9..4926419 100644
--- a/speedtest/request_test.cc
+++ b/speedtest/request_test.cc
@@ -14,10 +14,9 @@
  * limitations under the License.
  */
 
-#include <gtest/gtest.h>
-
 #include "request.h"
 
+#include <gtest/gtest.h>
 #include <memory>
 #include "curl_env.h"
 
@@ -30,12 +29,12 @@
   std::unique_ptr<Request> request;
 
   void SetUp() override {
-    env = std::make_shared<CurlEnv>();
-    request = env->NewRequest();
+    env = CurlEnv::NewCurlEnv({});
   }
 
   void VerifyQueryString(const char *expected,
                          Request::QueryStringParams params) {
+    request = env->NewRequest(Url());
     request->params() = params;
     request->UpdateUrl();
     EXPECT_EQ(expected, request->url().query_string());
@@ -44,7 +43,7 @@
   void VerifyUrl(const char *expected,
                  const char *url,
                  Request::QueryStringParams params) {
-    request->set_url(Url(url));
+    request = env->NewRequest(Url(url));
     request->params() = params;
     request->UpdateUrl();
     EXPECT_EQ(expected, request->url().url());
diff --git a/speedtest/runner.cc b/speedtest/runner.cc
new file mode 100644
index 0000000..0dae542
--- /dev/null
+++ b/speedtest/runner.cc
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "runner.h"
+
+#include <cassert>
+#include <thread>
+
+namespace speedtest {
+
+void TimedRun(GenericTest *test, long millis) {
+  assert(test);
+  test->Run();
+  std::thread timer([=] {
+    std::this_thread::sleep_for(
+        std::chrono::milliseconds(millis));
+    test->Stop();
+  });
+  timer.join();
+}
+
+}  // namespace speedtest
diff --git a/speedtest/runner.h b/speedtest/runner.h
new file mode 100644
index 0000000..4d677fc
--- /dev/null
+++ b/speedtest/runner.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SPEEDTEST_RUNNER_H
+#define SPEEDTEST_RUNNER_H
+
+#include "generic_test.h"
+
+namespace speedtest {
+
+void TimedRun(GenericTest *test, long millis);
+
+}  // namespace speedtest
+
+#endif  // SPEEDTEST_RUNNER_H
diff --git a/speedtest/speedtest.cc b/speedtest/speedtest.cc
index bf8fe3c..1cd9286 100644
--- a/speedtest/speedtest.cc
+++ b/speedtest/speedtest.cc
@@ -16,241 +16,332 @@
 
 #include "speedtest.h"
 
-#include <assert.h>
 #include <chrono>
 #include <cstring>
 #include <limits>
 #include <random>
 #include <thread>
 #include <iomanip>
+#include <fstream>
+#include <streambuf>
 
 #include "errors.h"
+#include "runner.h"
 #include "utils.h"
 
 namespace speedtest {
+namespace {
 
-Speedtest::Speedtest(const Options &options)
-    : options_(options),
-      bytes_downloaded_(0),
-      bytes_uploaded_(0) {
-  assert(!options_.hosts.empty());
-  env_ = std::make_shared<http::CurlEnv>();
+std::shared_ptr<std::string> MakeRandomData(size_t size) {
   std::random_device rd;
   std::default_random_engine random_engine(rd());
   std::uniform_int_distribution<char> uniform_dist(1, 255);
-  char *data = new char[options_.upload_size];
-  for (int i = 0; i < options_.upload_size; ++i) {
-    data[i] = uniform_dist(random_engine);
+  auto random_data = std::make_shared<std::string>();
+  random_data->resize(size);
+  for (size_t i = 0; i < size; ++i) {
+    (*random_data)[i] = uniform_dist(random_engine);
   }
-  send_data_ = data;
+  return std::move(random_data);
+}
+
+const char *kFileSerial = "/etc/serial";
+const char *kFileVersion = "/etc/version";
+
+std::string LoadFile(const std::string &file_name) {
+  std::ifstream in(file_name);
+  return std::string(std::istreambuf_iterator<char>(in),
+                     std::istreambuf_iterator<char>());
+}
+
+}  // namespace
+
+Speedtest::Speedtest(const Options &options)
+    : options_(options) {
+  http::CurlEnv::Options curl_options;
+  curl_options.disable_dns_cache = options_.disable_dns_cache;
+  curl_options.max_connections = options_.max_connections;
+  env_ = http::CurlEnv::NewCurlEnv(curl_options);
 }
 
 Speedtest::~Speedtest() {
-  delete[] send_data_;
 }
 
 void Speedtest::Run() {
-  if (!RunPingTest()) {
+  InitUserAgent();
+  LoadServerList();
+  if (servers_.empty()) {
+    std::cerr << "No servers found in global server list\n";
+    std::exit(1);
+  }
+  FindNearestServer();
+  if (!server_url_) {
     std::cout << "No servers responded. Exiting\n";
     return;
   }
+  std::string json = LoadConfig(*server_url_);
+  if (!ParseConfig(json, &config_)) {
+    std::cout << "Could not parse config\n";
+    return;
+  }
+  if (options_.verbose) {
+    std::cout << "Server config:\n";
+    PrintConfig(config_);
+  }
+  std::cout << "Location: " << config_.location_name << "\n";
+  std::cout << "URL: " << server_url_->url() << "\n";
   RunDownloadTest();
   RunUploadTest();
+  RunPingTest();
+}
+
+void Speedtest::InitUserAgent() {
+  if (options_.user_agent.empty()) {
+    std::string serial = LoadFile(kFileSerial);
+    std::string version = LoadFile(kFileVersion);
+    Trim(&serial);
+    Trim(&version);
+    user_agent_ = std::string("CPE/") +
+                  (version.empty() ? "unknown version" : version) + "/" +
+                  (serial.empty() ? "unknown serial" : serial);
+  } else {
+    user_agent_ = options_.user_agent;
+    return;
+  }
+  if (options_.verbose) {
+    std::cout << "Setting user agent to " << user_agent_ << "\n";
+  }
+}
+
+void Speedtest::LoadServerList() {
+  servers_.clear();
+  if (!options_.global) {
+    if (options_.verbose) {
+      std::cout << "Explicit server list:\n";
+      for (const auto &url : options_.hosts) {
+        std::cout << "  " << url.url() << "\n";
+      }
+    }
+    servers_ = options_.hosts;
+    return;
+  }
+
+  std::string json = LoadConfig(options_.global_host);
+  if (json.empty()) {
+    std::cerr << "Failed to load config JSON\n";
+    std::exit(1);
+  }
+  if (options_.verbose) {
+    std::cout << "Loaded config JSON: " << json << "\n";
+  }
+  if (!ParseServers(json, &servers_)) {
+    std::cerr << "Failed to parse server list: " << json << "\n";
+    std::exit(1);
+  }
+  if (options_.verbose) {
+    std::cout << "Loaded servers:\n";
+    for (const auto &url : servers_) {
+      std::cout << "  " << url.url() << "\n";
+    }
+  }
+}
+
+void Speedtest::FindNearestServer() {
+  server_url_.reset();
+  if (servers_.size() == 1) {
+    server_url_.reset(new http::Url(servers_[0]));
+    if (options_.verbose) {
+      std::cout << "Only 1 server so using " << server_url_->url() << "\n";
+    }
+    return;
+  }
+
+  PingTest::Options options;
+  options.verbose = options_.verbose;
+  options.timeout = PingTimeout();
+  std::vector<http::Url> hosts;
+  for (const auto &server : servers_) {
+    http::Url url(server);
+    url.set_path("/ping");
+    hosts.emplace_back(url);
+  }
+  options.num_pings = hosts.size();
+  if (options_.verbose) {
+    std::cout << "There are " << hosts.size() << " ping URLs:\n";
+    for (const auto &host : hosts) {
+      std::cout << "  " << host.url() << "\n";
+    }
+  }
+  options.request_factory = [&](int id) -> GenericTest::RequestPtr{
+    return MakeRequest(hosts[id]);
+  };
+  PingTest find_nearest(options);
+  if (options_.verbose) {
+    std::cout << "Starting to find nearest server\n";
+  }
+  TimedRun(&find_nearest, 1500);
+  find_nearest.WaitForEnd();
+  if (find_nearest.IsSucceeded()) {
+    PingStats fastest = find_nearest.GetFastest();
+    server_url_.reset(new http::Url(fastest.url));
+    server_url_->clear_path();
+    if (options_.verbose) {
+      double ping_millis = fastest.min_micros / 1000.0d;
+      std::cout << "Found nearest server: " << fastest.url.url()
+                   << " (" << round(ping_millis, 2) << " ms)\n";
+    }
+  }
+}
+
+std::string Speedtest::LoadConfig(const http::Url &url) {
+  http::Url config_url(url);
+  config_url.set_path("/config");
+  if (options_.verbose) {
+    std::cout << "Loading config from " << config_url.url() << "\n";
+  }
+  GenericTest::RequestPtr request = MakeRequest(config_url);
+  request->set_url(config_url);
+  std::string json;
+  request->Get([&](void *data, size_t size){
+    json.assign(static_cast<const char *>(data), size);
+  });
+  return json;
+}
+
+void Speedtest::RunPingTest() {
+  PingTest::Options options;
+  options.verbose = options_.verbose;
+  options.timeout = PingTimeout();
+  options.num_pings = 1;
+  http::Url ping_url(*server_url_);
+  ping_url.set_path("/ping");
+  options.request_factory = [&](int id) -> GenericTest::RequestPtr{
+    return MakeRequest(ping_url);
+  };
+  ping_test_.reset(new PingTest(options));
+  TimedRun(ping_test_.get(), PingRunTime());
+  ping_test_->WaitForEnd();
+  PingStats fastest = ping_test_->GetFastest();
+  if (ping_test_->IsSucceeded()) {
+    long micros = fastest.min_micros;
+    std::cout << "Ping time: " << round(micros / 1000.0d, 3) << " ms\n";
+  } else {
+    std::cout << "Failed to get ping response from "
+              << config_.location_name << " (" << fastest.url << ")\n";
+  }
 }
 
 void Speedtest::RunDownloadTest() {
-  end_download_ = false;
-  long start_time = SystemTimeMicros();
-  bytes_downloaded_ = 0;
-  std::thread threads[options_.number];
-  for (int i = 0; i < options_.number; ++i) {
-    threads[i] = std::thread([=]() {
-      RunDownload(i);
-    });
+  if (options_.verbose) {
+    std::cout << "Starting download test at " << config_.location_name
+              << " (" << server_url_->url() << ")\n";
   }
-  std::thread timer([&]{
-    std::this_thread::sleep_for(
-        std::chrono::milliseconds(options_.time_millis));
-    end_download_ = true;
-  });
-  timer.join();
-  for (auto &thread : threads) {
-    thread.join();
+  DownloadTest::Options options;
+  options.verbose = options_.verbose;
+  options.num_transfers = NumDownloads();
+  options.download_size = DownloadSize();
+  options.request_factory = [this](int id) -> GenericTest::RequestPtr{
+    return MakeTransferRequest(id, "/download");
+  };
+  download_test_.reset(new DownloadTest(options));
+  TimedRun(download_test_.get(), 5000);
+  download_test_->WaitForEnd();
+  long bytes = download_test_->bytes_transferred();
+  long micros = download_test_->GetRunningTime();
+  if (options_.verbose) {
+    std::cout << "Downloaded " << bytes << " bytes in "
+              << round(micros / 1000.0d, 2) << " ms\n";
   }
-  long end_time = speedtest::SystemTimeMicros();
-
-  double running_time = (end_time - start_time) / 1000000.0;
-  double megabits = bytes_downloaded_ * 8 / 1000000.0 / running_time;
-  std::cout << "Downloaded " << bytes_downloaded_
-            << " bytes in " << running_time * 1000 << " ms ("
-            << megabits << " Mbps)\n";
-}
-
-void Speedtest::RunDownload(int id) {
-  auto download = MakeRequest(id, "/download");
-  http::Request::DownloadFn noop = [](void *, size_t) {};
-  while (!end_download_) {
-    long downloaded = 0;
-    download->set_param("i", speedtest::to_string(id));
-    download->set_param("size", speedtest::to_string(options_.download_size));
-    download->set_param("time", speedtest::to_string(SystemTimeMicros()));
-    download->set_progress_fn([&](curl_off_t,
-                                  curl_off_t dlnow,
-                                  curl_off_t,
-                                  curl_off_t) -> bool {
-      if (dlnow > downloaded) {
-        bytes_downloaded_ += dlnow - downloaded;
-        downloaded = dlnow;
-      }
-      return end_download_;
-    });
-    download->Get(noop);
-    download->Reset();
-  }
+  std::cout << "Download speed: "
+            << round(ToMegabits(bytes, micros), 3) << " Mbps\n";
 }
 
 void Speedtest::RunUploadTest() {
-  end_upload_ = false;
-  long start_time = SystemTimeMicros();
-  bytes_uploaded_ = 0;
-  std::thread threads[options_.number];
-  for (int i = 0; i < options_.number; ++i) {
-    threads[i] = std::thread([=]() {
-      RunUpload(i);
-    });
-  }
-  std::thread timer([&]{
-    std::this_thread::sleep_for(
-        std::chrono::milliseconds(options_.time_millis));
-    end_upload_ = true;
-  });
-  timer.join();
-  for (auto &thread : threads) {
-    thread.join();
-  }
-  long end_time = speedtest::SystemTimeMicros();
-
-  double running_time = (end_time - start_time) / 1000000.0;
-  double megabits = bytes_uploaded_ * 8 / 1000000.0 / running_time;
-  std::cout << "Uploaded " << bytes_uploaded_
-            << " bytes in " << running_time * 1000 << " ms ("
-            << megabits << " Mbps)\n";
-}
-
-void Speedtest::RunUpload(int id) {
-  auto upload = MakeRequest(id, "/upload");
-  while (!end_upload_) {
-    long uploaded = 0;
-    upload->set_progress_fn([&](curl_off_t,
-                                curl_off_t,
-                                curl_off_t,
-                                curl_off_t ulnow) -> bool {
-      if (ulnow > uploaded) {
-        bytes_uploaded_ += ulnow - uploaded;
-        uploaded = ulnow;
-      }
-      return end_upload_;
-    });
-
-    // disable the Expect header as the server isn't expecting it (perhaps
-    // it should?). If the server isn't then libcurl waits for 1 second
-    // before sending the data anyway. So sending this header eliminated
-    // the 1 second delay.
-    upload->set_header("Expect", "");
-    upload->Post(send_data_, options_.upload_size);
-    upload->Reset();
-  }
-}
-
-bool Speedtest::RunPingTest() {
-  end_ping_ = false;
-  size_t num_hosts = options_.hosts.size();
-  std::thread threads[num_hosts];
-  min_ping_micros_.clear();
-  min_ping_micros_.resize(num_hosts);
-  for (size_t i = 0; i < num_hosts; ++i) {
-    threads[i] = std::thread([=]() {
-      RunPing(i);
-    });
-  }
-  std::thread timer([&]{
-    std::this_thread::sleep_for(
-        std::chrono::milliseconds(options_.time_millis));
-    end_ping_ = true;
-  });
-  timer.join();
-  for (auto &thread : threads) {
-    thread.join();
-  }
   if (options_.verbose) {
-    std::cout << "Pinged " << num_hosts << " "
-              << (num_hosts == 1 ? "host" : "hosts:") << "\n";
+    std::cout << "Starting upload test at " << config_.location_name
+              << " (" << server_url_->url() << ")\n";
   }
-  size_t min_index = 0;
-  for (size_t i = 0; i < num_hosts; ++i) {
-    if (options_.verbose) {
-      std::cout << "  " << options_.hosts[i].url() << ": ";
-      if (min_ping_micros_[i] == std::numeric_limits<long>::max()) {
-        std::cout << "no packets received";
-      } else {
-        double ping_ms = min_ping_micros_[i] / 1000.0;
-        if (ping_ms < 10) {
-          std::cout << std::fixed << std::setprecision(1);
-        } else {
-          std::cout << std::fixed << std::setprecision(0);
-        }
-        std::cout << ping_ms << " ms";
-      }
-      std::cout << "\n";
-    }
-    if (min_ping_micros_[i] < min_ping_micros_[min_index]) {
-      min_index = i;
-    }
+  UploadTest::Options options;
+  options.verbose = options_.verbose;
+  options.num_transfers = NumUploads();
+  options.payload = MakeRandomData(UploadSize());
+  options.request_factory = [this](int id) -> GenericTest::RequestPtr{
+    return MakeTransferRequest(id, "/upload");
+  };
+  upload_test_.reset(new UploadTest(options));
+  TimedRun(upload_test_.get(), 5000);
+  upload_test_->WaitForEnd();
+  long bytes = upload_test_->bytes_transferred();
+  long micros = upload_test_->GetRunningTime();
+  if (options_.verbose) {
+    std::cout << "Uploaded " << bytes << " bytes in "
+              << round(micros / 1000.0d, 2) << " ms\n";
   }
-  if (min_ping_micros_[min_index] == std::numeric_limits<long>::max()) {
-    // no servers respondeded
-    return false;
-  }
-  url_ = options_.hosts[min_index];
-  std::cout << "Host for Speedtest: " << url_.url() << " (";
-  double ping_ms = min_ping_micros_[min_index] / 1000.0;
-  if (ping_ms < 10) {
-    std::cout << std::fixed << std::setprecision(1);
-  } else {
-    std::cout << std::fixed << std::setprecision(0);
-  }
-  std::cout << ping_ms << " ms)\n";
-  return true;
+  std::cout << "Upload speed: "
+            << round(ToMegabits(bytes, micros), 3) << " Mbps\n";
 }
 
-void Speedtest::RunPing(size_t index) {
-  http::Request::DownloadFn noop = [](void *, size_t) {};
-  min_ping_micros_[index] = std::numeric_limits<long>::max();
-  http::Url url(options_.hosts[index]);
-  url.set_path("/ping");
-  auto ping = env_->NewRequest();
-  ping->set_url(url);
-  while (!end_ping_) {
-    long req_start = SystemTimeMicros();
-    if (ping->Get(noop) == CURLE_OK) {
-      long req_end = SystemTimeMicros();
-      long ping_time = req_end - req_start;
-      min_ping_micros_[index] = std::min(min_ping_micros_[index], ping_time);
-    }
-    ping->Reset();
-    std::this_thread::sleep_for(std::chrono::milliseconds(100));
-  }
+int Speedtest::NumDownloads() const {
+  return options_.num_downloads
+         ? options_.num_downloads
+         : config_.num_downloads;
 }
 
-std::unique_ptr<http::Request> Speedtest::MakeRequest(int id,
-                                                      const std::string &path) {
-  auto request = env_->NewRequest();
-  http::Url url(url_);
-  int port = (id % 20) + url.port() + 1;
-  url.set_port(port);
-  url.set_path(path);
-  request->set_url(url);
+int Speedtest::DownloadSize() const {
+  return options_.download_size
+         ? options_.download_size
+         : config_.download_size;
+}
+
+int Speedtest::NumUploads() const {
+  return options_.num_uploads
+         ? options_.num_uploads
+         : config_.num_uploads;
+}
+
+int Speedtest::UploadSize() const {
+  return options_.upload_size
+         ? options_.upload_size
+         : config_.upload_size;
+}
+
+int Speedtest::PingRunTime() const {
+  return options_.ping_runtime
+         ? options_.ping_runtime
+         : config_.ping_runtime;
+}
+
+int Speedtest::PingTimeout() const {
+  return options_.ping_timeout
+         ? options_.ping_timeout
+         : config_.ping_timeout;
+}
+
+GenericTest::RequestPtr Speedtest::MakeRequest(const http::Url &url) {
+  GenericTest::RequestPtr request = env_->NewRequest(url);
+  if (!user_agent_.empty()) {
+    request->set_user_agent(user_agent_);
+  }
   return std::move(request);
 }
 
+GenericTest::RequestPtr Speedtest::MakeBaseRequest(
+    int id, const std::string &path) {
+  http::Url url(*server_url_);
+  url.set_path(path);
+  return MakeRequest(url);
+}
+
+GenericTest::RequestPtr Speedtest::MakeTransferRequest(
+    int id, const std::string &path) {
+  http::Url url(*server_url_);
+  int port_start = config_.transfer_port_start;
+  int port_end = config_.transfer_port_end;
+  int num_ports = port_end - port_start + 1;
+  if (num_ports > 0) {
+    url.set_port(port_start + (id % num_ports));
+  }
+  url.set_path(path);
+  return MakeRequest(url);
+}
+
 }  // namespace speedtest
diff --git a/speedtest/speedtest.h b/speedtest/speedtest.h
index 01e6f75..ea509f7 100644
--- a/speedtest/speedtest.h
+++ b/speedtest/speedtest.h
@@ -21,8 +21,13 @@
 #include <memory>
 #include <string>
 
+#include "config.h"
 #include "curl_env.h"
+#include "download_test.h"
+#include "generic_test.h"
 #include "options.h"
+#include "ping_test.h"
+#include "upload_test.h"
 #include "url.h"
 #include "request.h"
 
@@ -34,27 +39,37 @@
   virtual ~Speedtest();
 
   void Run();
-  void RunDownloadTest();
-  void RunUploadTest();
-  bool RunPingTest();
 
  private:
-  void RunDownload(int id);
-  void RunUpload(int id);
-  void RunPing(size_t host_index);
+  void InitUserAgent();
+  void LoadServerList();
+  void FindNearestServer();
+  std::string LoadConfig(const http::Url &url);
+  void RunPingTest();
+  void RunDownloadTest();
+  void RunUploadTest();
 
-  std::unique_ptr<http::Request> MakeRequest(int id, const std::string &path);
+  int NumDownloads() const;
+  int DownloadSize() const;
+  int NumUploads() const;
+  int UploadSize() const;
+  int PingTimeout() const;
+  int PingRunTime() const;
 
-  std::shared_ptr<http::CurlEnv> env_;
+  GenericTest::RequestPtr MakeRequest(const http::Url &url);
+  GenericTest::RequestPtr MakeBaseRequest(int id, const std::string &path);
+  GenericTest::RequestPtr MakeTransferRequest(int id, const std::string &path);
+
+  std::shared_ptr <http::CurlEnv> env_;
   Options options_;
-  http::Url url_;
-  std::atomic_bool end_ping_;
-  std::atomic_bool end_download_;
-  std::atomic_bool end_upload_;
-  std::atomic_long bytes_downloaded_;
-  std::atomic_long bytes_uploaded_;
-  std::vector<long> min_ping_micros_;
-  const char *send_data_;
+  Config config_;
+  std::string user_agent_;
+  std::vector<http::Url> servers_;
+  std::unique_ptr<http::Url> server_url_;
+  std::unique_ptr<std::string> send_data_;
+  std::unique_ptr<PingTest> ping_test_;
+  std::unique_ptr<DownloadTest> download_test_;
+  std::unique_ptr<UploadTest> upload_test_;
 
   // disable
   Speedtest(const Speedtest &) = delete;
diff --git a/speedtest/transfer_test.cc b/speedtest/transfer_test.cc
new file mode 100644
index 0000000..213d5d9
--- /dev/null
+++ b/speedtest/transfer_test.cc
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "transfer_test.h"
+
+#include <cassert>
+
+namespace speedtest {
+
+TransferTest::TransferTest(const Options &options)
+    : GenericTest(options),
+      bytes_transferred_(0),
+      requests_started_(0),
+      requests_ended_(0) {
+  assert(options.num_transfers > 0);
+}
+
+void TransferTest::ResetCounters() {
+  bytes_transferred_ = 0;
+  requests_started_ = 0;
+  requests_ended_ = 0;
+}
+
+void TransferTest::StartRequest() {
+  requests_started_++;
+}
+
+void TransferTest::EndRequest() {
+  requests_ended_++;
+}
+
+void TransferTest::TransferBytes(long bytes) {
+  bytes_transferred_ += bytes;
+}
+
+}  // namespace speedtest
diff --git a/speedtest/transfer_test.h b/speedtest/transfer_test.h
new file mode 100644
index 0000000..d7307a6
--- /dev/null
+++ b/speedtest/transfer_test.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SPEEDTEST_TRANSFER_TEST_H
+#define SPEEDTEST_TRANSFER_TEST_H
+
+#include <atomic>
+#include "generic_test.h"
+
+namespace speedtest {
+
+class TransferTest : public GenericTest {
+ public:
+  struct Options : GenericTest::Options {
+    int num_transfers;
+  };
+
+  explicit TransferTest(const Options &options);
+
+  long bytes_transferred() const { return bytes_transferred_; }
+  long requests_started() const { return requests_started_; }
+  long requests_ended() const { return requests_ended_; }
+
+ protected:
+  void ResetCounters();
+  void StartRequest();
+  void EndRequest();
+  void TransferBytes(long bytes);
+
+ private:
+  std::atomic_long bytes_transferred_;
+  std::atomic_int requests_started_;
+  std::atomic_int requests_ended_;
+
+  // disallowed
+  TransferTest(const TransferTest &) = delete;
+  void operator=(const TransferTest &) = delete;
+};
+
+}  // namespace speedtest
+
+#endif  // SPEEDTEST_TRANSFER_TEST_H
diff --git a/speedtest/upload_test.cc b/speedtest/upload_test.cc
new file mode 100644
index 0000000..8c014ba
--- /dev/null
+++ b/speedtest/upload_test.cc
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "upload_test.h"
+
+#include <algorithm>
+#include <cassert>
+#include <iostream>
+#include "generic_test.h"
+#include "utils.h"
+
+namespace speedtest {
+
+UploadTest::UploadTest(const Options &options)
+    : TransferTest(options),
+      options_(options) {
+  assert(options_.payload);
+  assert(options_.payload->size() > 0);
+}
+
+void UploadTest::RunInternal() {
+  ResetCounters();
+  threads_.clear();
+  if (options_.verbose) {
+    std::cout << "Uploading " << options_.num_transfers
+              << " threads with " << options_.payload->size() << " bytes\n";
+  }
+  for (int i = 0; i < options_.num_transfers; ++i) {
+    threads_.emplace_back([=]{
+      RunUpload(i);
+    });
+  }
+}
+
+void UploadTest::StopInternal() {
+  std::for_each(threads_.begin(), threads_.end(), [](std::thread &t) {
+    t.join();
+  });
+}
+
+void UploadTest::RunUpload(int id) {
+  GenericTest::RequestPtr upload = options_.request_factory(id);
+  while (GetStatus() == TestStatus::RUNNING) {
+    long uploaded = 0;
+    upload->set_param("i", to_string(id));
+    upload->set_param("time", to_string(SystemTimeMicros()));
+    upload->set_progress_fn([&](curl_off_t,
+                                  curl_off_t,
+                                  curl_off_t,
+                                  curl_off_t ulnow) -> bool {
+      if (ulnow > uploaded) {
+        TransferBytes(ulnow - uploaded);
+        uploaded = ulnow;
+      }
+      return GetStatus() != TestStatus::RUNNING;
+    });
+
+    // disable the Expect header as the server isn't expecting it (perhaps
+    // it should?). If the server isn't then libcurl waits for 1 second
+    // before sending the data anyway. So sending this header eliminated
+    // the 1 second delay.
+    upload->set_header("Expect", "");
+
+    StartRequest();
+    upload->Post(options_.payload->c_str(), options_.payload->size());
+    EndRequest();
+    upload->Reset();
+  }
+}
+
+}  // namespace speedtest
diff --git a/speedtest/upload_test.h b/speedtest/upload_test.h
new file mode 100644
index 0000000..509eed6
--- /dev/null
+++ b/speedtest/upload_test.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SPEEDTEST_UPLOAD_TEST_H
+#define SPEEDTEST_UPLOAD_TEST_H
+
+#include <memory>
+#include <string>
+#include <thread>
+#include <vector>
+#include "transfer_test.h"
+
+namespace speedtest {
+
+class UploadTest : public TransferTest {
+ public:
+  struct Options : TransferTest::Options {
+    std::shared_ptr<std::string> payload;
+  };
+
+  explicit UploadTest(const Options &options);
+
+ protected:
+  void RunInternal() override;
+  void StopInternal() override;
+
+ private:
+  void RunUpload(int id);
+
+  Options options_;
+  std::vector<std::thread> threads_;
+
+  // disallowed
+  UploadTest(const UploadTest &) = delete;
+  void operator=(const UploadTest &) = delete;
+};
+
+}  // namespace speedtest
+
+#endif  // SPEEDTEST_UPLOAD_TEST_H
diff --git a/speedtest/url.cc b/speedtest/url.cc
index c0934a0..6292790 100644
--- a/speedtest/url.cc
+++ b/speedtest/url.cc
@@ -132,6 +132,10 @@
   return url1.url() == url2.url();
 }
 
+std::ostream &operator<<(std::ostream &os, const Url &url) {
+  return os << (url.ok() ? url.url() : "{invalid URL}");
+}
+
 Url::Url(): parsed_(false), absolute_(false), port_(0) {
 }
 
@@ -150,7 +154,8 @@
   fragment_ = other.fragment_;
 }
 
-Url::Url(const char *url): parsed_(false), absolute_(false), port_(0) {
+Url::Url(const std::string &url)
+    : parsed_(false), absolute_(false), port_(0) {
   Parse(url);
 }
 
@@ -411,8 +416,9 @@
     return false;
   }
   std::string port(start, iter);
-  int portnum = speedtest::stoi(port);
-  if (portnum < 1 || portnum > 65535) {
+  int portnum;
+  if (!speedtest::ParseInt(port, &portnum) ||
+      portnum < 1 || portnum > 65535) {
     return false;
   }
   current_ = iter;
diff --git a/speedtest/url.h b/speedtest/url.h
index 6aeb68a..b02bdd0 100644
--- a/speedtest/url.h
+++ b/speedtest/url.h
@@ -24,6 +24,7 @@
 class Url;
 
 bool operator==(const Url &url1, const Url &url2);
+std::ostream &operator<<(std::ostream &os, const Url &url);
 
 // Partial implementation of a URL parser. This is needed because URLs need
 // to be manipulated for creating URLs for Speedtest, which is otherwise
@@ -43,7 +44,7 @@
  public:
   Url();
   Url(const Url &other);
-  explicit Url(const char *url);
+  explicit Url(const std::string &url);
   Url &operator=(const Url &other);
 
   bool Parse(const std::string &url);
@@ -75,6 +76,7 @@
   std::string url() const;
 
   friend bool operator==(const Url &url1, const Url &url2);
+  friend std::ostream &operator<<(std::ostream &os, const Url &url);
 
  private:
   using Iter = std::string::const_iterator;
diff --git a/speedtest/url_test.cc b/speedtest/url_test.cc
index 06c2d72..b43d4fb 100644
--- a/speedtest/url_test.cc
+++ b/speedtest/url_test.cc
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-#include <gtest/gtest.h>
-
 #include "url.h"
 
+#include <gtest/gtest.h>
+
 namespace http {
 namespace {
 
diff --git a/speedtest/utils.cc b/speedtest/utils.cc
index 012e1eb..b41bb1f 100644
--- a/speedtest/utils.cc
+++ b/speedtest/utils.cc
@@ -16,9 +16,13 @@
 
 #include "utils.h"
 
+#include <algorithm>
+#include <cctype>
 #include <cstdlib>
+#include <functional>
 #include <iostream>
 #include <stdexcept>
+#include <stdio.h>
 #include <string>
 #include <sstream>
 
@@ -33,24 +37,53 @@
   return ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
 }
 
-std::string to_string(long n)
-{
+std::string to_string(long n) {
   std::ostringstream s;
   s << n;
   return s.str();
 }
 
-int stoi(const std::string& str)
-{
-  int rc;
-  std::istringstream n(str);
+std::string round(double d, int digits) {
+  char buf[20];
+  sprintf(buf, "%.*f", digits, d);
+  return buf;
+}
 
-  if (!(n >> rc)) {
-    std::cerr << "Not a number: " << str;
-    std::exit(1);
+double ToMegabits(long bytes, long micros) {
+  return (8.0d * bytes) / micros;
+}
+
+bool ParseInt(const std::string &str, int *result) {
+  if (!result) {
+    return false;
   }
+  std::istringstream n(str);
+  return n >> *result;
+}
 
-  return rc;
+// Trim from start in place
+// Caller retains ownership
+void LeftTrim(std::string *s) {
+  s->erase(s->begin(),
+           std::find_if(s->begin(),
+                        s->end(),
+                        std::not1(std::ptr_fun<int, int>(std::isspace))));
+}
+
+// Trim from end in place
+// Caller retains ownership
+void RightTrim(std::string *s) {
+  s->erase(std::find_if(s->rbegin(),
+                        s->rend(),
+                        std::not1(std::ptr_fun<int, int>(std::isspace))).base(),
+           s->end());
+}
+
+// Trim from both ends in place
+// Caller retains ownership
+void Trim(std::string *s) {
+  LeftTrim(s);
+  RightTrim(s);
 }
 
 }  // namespace speedtest
diff --git a/speedtest/utils.h b/speedtest/utils.h
index 7ad4289..f3e5f1f 100644
--- a/speedtest/utils.h
+++ b/speedtest/utils.h
@@ -28,8 +28,28 @@
 // Return a string representation of n
 std::string to_string(long n);
 
-// return an integer value from the string str.
-int stoi(const std::string& str);
+// Round a double to a minimum number of significant digits
+std::string round(double d, int digits);
+
+// Convert bytes and time in micros to speed in megabits
+double ToMegabits(long bytes, long micros);
+
+// Parse an int.
+// If successful, write result to result and return true.
+// If result is null or the int can't be parsed, return false.
+bool ParseInt(const std::string &str, int *result);
+
+// Trim from start in place
+// Caller retains ownership
+void LeftTrim(std::string *s);
+
+// Trim from end in place
+// Caller retains ownership
+void RightTrim(std::string *s);
+
+// Trim from both ends in place
+// Caller retains ownership
+void Trim(std::string *s);
 
 }  // namespace speedtst