Add Speedtest result reporting

Change-Id: I7822e08bf10ea8ee0b2823f30ae9c7d3c5d7e4ba
diff --git a/speedtest/Makefile b/speedtest/Makefile
index 51974cc..f926c9a 100644
--- a/speedtest/Makefile
+++ b/speedtest/Makefile
@@ -24,6 +24,7 @@
      ping.o \
      region.o \
      request.o \
+     result.o \
      speedtest.o \
      status.o \
      transfer_runner.o \
@@ -82,14 +83,25 @@
           region.h \
           utils.h
 request.o: request.cc request.h url.h utils.h
+result.o: result.cc \
+          result.h \
+          config.h \
+          find_nearest.h \
+          init.h \
+          ping.h \
+          speedtest.h \
+          transfer_runner.h \
+          url.h
 speedtest.o: speedtest.cc \
              speedtest.h \
              config.h \
              download.h \
+             errors.h \
              init.h \
              options.h \
              region.h \
              request.h \
+             result.h \
              status.h \
              timed_runner.h \
              transfer_runner.h \
diff --git a/speedtest/config.cc b/speedtest/config.cc
index 0615a75..41803eb 100644
--- a/speedtest/config.cc
+++ b/speedtest/config.cc
@@ -85,6 +85,7 @@
   config->ping_timeout_millis = root["pingTimeout"].asInt();
   config->transfer_port_start = root["transferPortStart"].asInt();
   config->transfer_port_end = root["transferPortEnd"].asInt();
+  config->average_type = root["averageType"].asString();
   return Status::OK;
 }
 
@@ -108,7 +109,8 @@
       << "Ping runtime: " << config.ping_runtime_millis << " ms\n"
       << "Ping timeout: " << config.ping_timeout_millis << " ms\n"
       << "Transfer port start: " << config.transfer_port_start << "\n"
-      << "Transfer port end: " << config.transfer_port_end << "\n";
+      << "Transfer port end: " << config.transfer_port_end << "\n"
+      << "Average type: " << config.average_type << "\n";
 }
 
 }  // namespace
diff --git a/speedtest/config.h b/speedtest/config.h
index f20d812..f03f6cf 100644
--- a/speedtest/config.h
+++ b/speedtest/config.h
@@ -44,6 +44,7 @@
   long ping_timeout_millis = 0;
   int transfer_port_start = 0;
   int transfer_port_end = 0;
+  std::string average_type;
 };
 
 struct ConfigOptions {
diff --git a/speedtest/init.cc b/speedtest/init.cc
index 555a2ea..39b646b 100644
--- a/speedtest/init.cc
+++ b/speedtest/init.cc
@@ -102,9 +102,6 @@
       std::cout << "Load config failed: " << result.status.ToString() << "\n";
     }
   } else {
-    if (options_.verbose) {
-      PrintConfig(result.config_result.config);
-    }
     result.status = Status::OK;
     if (result.selected_region.id.empty()) {
       result.selected_region.id = result.config_result.config.location_id;
diff --git a/speedtest/result.cc b/speedtest/result.cc
new file mode 100644
index 0000000..e27e15a
--- /dev/null
+++ b/speedtest/result.cc
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2016 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 "result.h"
+
+#include "url.h"
+
+namespace speedtest {
+namespace {
+
+template <typename T>
+void PopulateDuration(Json::Value &json, const T &t) {
+  json["startMillis"] = static_cast<Json::Value::Int64>(t.start_time);
+  json["endMillis"] = static_cast<Json::Value::Int64>(t.end_time);
+}
+
+}  // namespace
+
+void PopulateParameters(Json::Value &json, const Config &config) {
+  json["downloadSize"] =
+      static_cast<Json::Value::Int64>(config.download_bytes);
+  json["uploadSize"] =
+      static_cast<Json::Value::Int64>(config.upload_bytes);
+  json["intervalSize"] =
+      static_cast<Json::Value::Int64>(config.interval_millis);
+  json["locationId"] = config.location_id;
+  json["locationName"] = config.location_name;
+  json["minTransferIntervals"] = config.min_transfer_intervals;
+  json["maxTransferIntervals"] = config.max_transfer_intervals;
+  json["minTransferRunTime"] =
+      static_cast<Json::Value::Int64>(config.min_transfer_runtime);
+  json["maxTransferRunTime"] =
+      static_cast<Json::Value::Int64>(config.max_transfer_runtime);
+  json["maxTransferVariance"] = config.max_transfer_variance;
+  json["numConcurrentDownloads"] = config.num_downloads;
+  json["numConcurrentUploads"] = config.num_uploads;
+  json["pingRunTime"] =
+      static_cast<Json::Value::Int64>(config.ping_runtime_millis);
+  json["pingTimeout"] =
+      static_cast<Json::Value::Int64>(config.ping_timeout_millis);
+  json["transferPortStart"] = config.transfer_port_start;
+  json["transferPortEnd"] = config.transfer_port_end;
+  json["averageType"] = config.average_type;
+}
+
+void PopulateConfigResult(Json::Value &json,
+                          const ConfigResult &config_result) {
+  PopulateDuration(json, config_result);
+  PopulateParameters(json["parameters"], config_result.config);
+}
+
+void PopulateFindNearest(Json::Value &json,
+                         const FindNearest::Result &find_nearest) {
+  PopulateDuration(json, find_nearest);
+  json["pingResults"] = Json::Value(Json::arrayValue);
+  for (const Ping::Result &ping_result : find_nearest.ping_results) {
+    Json::Value ping;
+    ping["id"] = ping_result.region.id;
+    ping["url"] = ping_result.region.urls.front().url();
+    if (ping_result.received > 0) {
+      ping["minPingMillis"] =
+          static_cast<Json::Value::Int64>(ping_result.min_ping_micros);
+    }
+    json["pingResults"].append(ping);
+  }
+}
+
+void PopulateInitResult(Json::Value &json,
+                        const Init::Result &init_result) {
+  PopulateDuration(json, init_result);
+  PopulateConfigResult(json["configResult"], init_result.config_result);
+  if (!init_result.find_nearest_result.ping_results.empty()) {
+    PopulateFindNearest(json["findNearest"], init_result.find_nearest_result);
+  }
+  json["selectedRegion"] = init_result.selected_region.id;
+}
+
+void PopulateTransfer(Json::Value &json,
+                      const TransferResult &transfer_result) {
+  PopulateDuration(json, transfer_result);
+  json["speedMbps"] = transfer_result.speed_mbps;
+  json["totalBytes"] =
+      static_cast<Json::Value::Int64>(transfer_result.total_bytes);
+  json["buckets"] = Json::Value(Json::arrayValue);
+  for (const Bucket &bucket : transfer_result.buckets) {
+    Json::Value bucket_json;
+    bucket_json["totalBytes"] =
+        static_cast<Json::Value::Int64>(bucket.total_bytes);
+    bucket_json["longSpeedMbps"] = bucket.long_megabits;
+    bucket_json["shortSpeedMbps"] = bucket.short_megabits;
+    bucket_json["offsetMillis"] = bucket.start_time / 1000.0d;
+    json["buckets"].append(bucket_json);
+  }
+}
+
+void PopulatePingResult(Json::Value &json, const Ping::Result &ping_result) {
+  PopulateDuration(json, ping_result);
+  json["id"] = ping_result.region.id;
+  json["url"] = ping_result.region.urls.front().url();
+  if (ping_result.received > 0) {
+    json["minPingMillis"] =
+        static_cast<Json::Value::Int64>(ping_result.min_ping_micros);
+  }
+}
+
+void PopulateSpeedtest(Json::Value &json,
+                       const Speedtest::Result &speedtest_result) {
+  PopulateDuration(json, speedtest_result);
+  PopulateInitResult(json["initResult"], speedtest_result.init_result);
+  if (speedtest_result.download_run) {
+    PopulateTransfer(json["downloadResult"], speedtest_result.download_result);
+  }
+  if (speedtest_result.upload_run) {
+    PopulateTransfer(json["uploadResult"], speedtest_result.upload_result);
+  }
+  if (speedtest_result.ping_run) {
+    PopulatePingResult(json["pingResult"], speedtest_result.ping_result);
+  }
+  json["endState"] = "COMPLETE";
+}
+
+}  // namespace speedtest
diff --git a/speedtest/result.h b/speedtest/result.h
new file mode 100644
index 0000000..4ffa07c
--- /dev/null
+++ b/speedtest/result.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 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_RESULT_H
+#define SPEEDTEST_RESULT_H
+
+#include <jsoncpp/json/json.h>
+#include "config.h"
+#include "find_nearest.h"
+#include "init.h"
+#include "ping.h"
+#include "speedtest.h"
+#include "transfer_runner.h"
+
+namespace speedtest {
+
+void PopulateParameters(Json::Value &json, const Config &config);
+void PopulateConfigResult(Json::Value &json,
+                          const ConfigResult &config_result);
+void PopulateFindNearest(Json::Value &json,
+                         const FindNearest::Result &find_nearest);
+void PopulateInitResult(Json::Value &json,
+                        const Init::Result &init_result);
+void PopulateTransfer(Json::Value &json,
+                      const TransferResult &transfer_result);
+void PopulatePingResult(Json::Value &json, const Ping::Result &ping_result);
+void PopulateSpeedtest(Json::Value &json,
+                       const Speedtest::Result &speedtest_result);
+
+}  // namespace speedtest
+
+#endif  // SPEEDTEST_RESULT_H
diff --git a/speedtest/speedtest.cc b/speedtest/speedtest.cc
index 2f233fa..93a0248 100644
--- a/speedtest/speedtest.cc
+++ b/speedtest/speedtest.cc
@@ -16,7 +16,12 @@
 
 #include "speedtest.h"
 
+#include <curl/curl.h>
+#include <jsoncpp/json/json.h>
+#include <jsoncpp/json/writer.h>
 #include "download.h"
+#include "errors.h"
+#include "result.h"
 #include "timed_runner.h"
 #include "upload.h"
 
@@ -52,20 +57,32 @@
     result.end_time = SystemTimeMicros();
     return result;
   }
+
   selected_region_ = result.init_result.selected_region;
   if (options_.verbose) {
     std::cout << "Setting selected region to "
               << DescribeRegion(selected_region_) << "\n";
   }
 
+  if (result.init_result.config_result.config.location_id.empty()) {
+    result.init_result.config_result.config.location_id = selected_region_.id;
+  }
+  if (result.init_result.config_result.config.location_name.empty()) {
+    result.init_result.config_result.config.location_name = selected_region_.name;
+  }
+
+  OverrideConfigWithOptions(&result.init_result.config_result.config, options_);
+  config_ = result.init_result.config_result.config;
+  if (options_.verbose) {
+    PrintConfig(config_);
+  }
+
   if (*cancel) {
     result.status = Status(StatusCode::ABORTED, "Speedtest aborted");
     result.end_time = SystemTimeMicros();
     return result;
   }
 
-  config_ = result.init_result.config_result.config;
-
   std::cout << "ID: " << result.init_result.selected_region.id << "\n";
   std::cout << "Location: " << result.init_result.selected_region.name << "\n";
 
@@ -125,9 +142,37 @@
               << ToMillis(result.ping_result.min_ping_micros)
               << " ms\n";
   }
-  
+
   result.status = Status::OK;
   result.end_time = SystemTimeMicros();
+
+  if (!options_.report_results) {
+    if (options_.verbose) {
+      std::cout << "Not reporting results to server\n";
+    }
+  } else {
+    Json::Value root;
+    PopulateSpeedtest(root, result);
+    Json::FastWriter writer;
+    std::string out = writer.write(root);
+
+    http::Url result_url(selected_region_.urls.front());
+    result_url.set_path("/result");
+    if (options_.verbose) {
+      std::cout << "Posting results to " << result_url.url() << "\n";
+    }
+    http::Request::Ptr request = options_.request_factory(result_url);
+    request->set_header("Content-Type", "application/json");
+    CURLcode curl_code = request->Post(out.c_str(), out.size());
+    if (curl_code == CURLE_OK) {
+      if (options_.verbose) {
+        std::cout << "Result posted successfully\n";
+      }
+    } else {
+      std::cout << "Failed to report results: "
+                << http::ErrorString(curl_code) << "\n";
+    }
+  }
   return result;
 }
 
@@ -138,8 +183,8 @@
   }
   Download::Options download_options;
   download_options.verbose = options_.verbose;
-  download_options.num_transfers = GetNumDownloads();
-  download_options.download_bytes = GetDownloadSizeBytes();
+  download_options.num_transfers = config_.num_downloads;
+  download_options.download_bytes = config_.download_bytes;
   download_options.request_factory = [this](int id) -> http::Request::Ptr{
     return MakeTransferRequest(id, "/download");
   };
@@ -147,12 +192,14 @@
 
   TransferOptions transfer_options;
   transfer_options.verbose = options_.verbose;
-  transfer_options.min_runtime_millis = GetMinTransferRunTimeMillis();
-  transfer_options.max_runtime_millis = GetMaxTransferRunTimeMillis();
-  transfer_options.min_intervals = GetMinTransferIntervals();
-  transfer_options.max_intervals = GetMaxTransferIntervals();
-  transfer_options.max_variance = GetMaxTransferVariance();
-  transfer_options.interval_millis = GetIntervalMillis();
+  transfer_options.min_runtime_millis = config_.min_transfer_runtime;
+  transfer_options.max_runtime_millis = config_.max_transfer_runtime;
+  transfer_options.min_intervals = config_.min_transfer_intervals;
+  transfer_options.max_intervals = config_.max_transfer_intervals;
+  transfer_options.max_variance = config_.max_transfer_variance;
+  transfer_options.interval_millis = config_.interval_millis;
+  transfer_options.exponential_moving_average =
+      config_.average_type == "EXPONENTIAL";
   if (options_.progress_millis > 0) {
     transfer_options.progress_millis = options_.progress_millis;
     transfer_options.progress_fn = [](Bucket bucket) {
@@ -175,8 +222,8 @@
   }
   Upload::Options upload_options;
   upload_options.verbose = options_.verbose;
-  upload_options.num_transfers = GetNumUploads();
-  upload_options.payload = MakeRandomData(GetUploadSizeBytes());
+  upload_options.num_transfers = config_.num_uploads;
+  upload_options.payload = MakeRandomData(config_.upload_bytes);
   upload_options.request_factory = [this](int id) -> http::Request::Ptr{
     return MakeTransferRequest(id, "/upload");
   };
@@ -184,12 +231,14 @@
 
   TransferOptions transfer_options;
   transfer_options.verbose = options_.verbose;
-  transfer_options.min_runtime_millis = GetMinTransferRunTimeMillis();
-  transfer_options.max_runtime_millis = GetMaxTransferRunTimeMillis();
-  transfer_options.min_intervals = GetMinTransferIntervals();
-  transfer_options.max_intervals = GetMaxTransferIntervals();
-  transfer_options.max_variance = GetMaxTransferVariance();
-  transfer_options.interval_millis = GetIntervalMillis();
+  transfer_options.min_runtime_millis = config_.min_transfer_runtime;
+  transfer_options.max_runtime_millis = config_.max_transfer_runtime;
+  transfer_options.min_intervals = config_.min_transfer_intervals;
+  transfer_options.max_intervals = config_.max_transfer_intervals;
+  transfer_options.max_variance = config_.max_transfer_variance;
+  transfer_options.interval_millis = config_.interval_millis;
+  transfer_options.exponential_moving_average =
+      config_.average_type == "EXPONENTIAL";
   if (options_.progress_millis > 0) {
     transfer_options.progress_millis = options_.progress_millis;
     transfer_options.progress_fn = [](Bucket bucket) {
@@ -208,86 +257,57 @@
 Ping::Result Speedtest::RunPingTest(std::atomic_bool *cancel) {
   Ping::Options ping_options;
   ping_options.verbose = options_.verbose;
-  ping_options.timeout_millis = GetPingTimeoutMillis();
+  ping_options.timeout_millis = config_.ping_timeout_millis;
   ping_options.region = selected_region_;
   ping_options.num_concurrent_pings = 0;
   ping_options.request_factory = [&](const http::Url &url){
     return MakeRequest(url);
   };
   Ping ping(ping_options);
-  return RunTimed(std::ref(ping), cancel, GetPingRunTimeMillis());
+  return RunTimed(std::ref(ping), cancel, config_.ping_runtime_millis);
 }
 
-int Speedtest::GetNumDownloads() const {
-  return options_.num_downloads
-         ? options_.num_downloads
-         : config_.num_downloads;
-}
-
-long Speedtest::GetDownloadSizeBytes() const {
-  return options_.download_bytes
-         ? options_.download_bytes
-         : config_.download_bytes;
-}
-
-int Speedtest::GetNumUploads() const {
-  return options_.num_uploads
-         ? options_.num_uploads
-         : config_.num_uploads;
-}
-
-long Speedtest::GetUploadSizeBytes() const {
-  return options_.upload_bytes
-         ? options_.upload_bytes
-         : config_.upload_bytes;
-}
-
-long Speedtest::GetPingRunTimeMillis() const {
-  return options_.ping_runtime_millis
-         ? options_.ping_runtime_millis
-         : config_.ping_runtime_millis;
-}
-
-long Speedtest::GetPingTimeoutMillis() const {
-  return options_.ping_timeout_millis
-         ? options_.ping_timeout_millis
-         : config_.ping_timeout_millis;
-}
-
-long Speedtest::GetMinTransferRunTimeMillis() const {
-  return options_.min_transfer_runtime
-         ? options_.min_transfer_runtime
-         : config_.min_transfer_runtime;
-}
-
-long Speedtest::GetMaxTransferRunTimeMillis() const {
-  return options_.max_transfer_runtime
-         ? options_.max_transfer_runtime
-         : config_.max_transfer_runtime;
-}
-
-int Speedtest::GetMinTransferIntervals() const {
-  return options_.min_transfer_intervals
-         ? options_.min_transfer_intervals
-         : config_.min_transfer_intervals;
-}
-
-int Speedtest::GetMaxTransferIntervals() const {
-  return options_.max_transfer_intervals
-         ? options_.max_transfer_intervals
-         : config_.max_transfer_intervals;
-}
-
-double Speedtest::GetMaxTransferVariance() const {
-  return options_.max_transfer_variance
-         ? options_.max_transfer_variance
-         : config_.max_transfer_variance;
-}
-
-long Speedtest::GetIntervalMillis() const {
-  return options_.interval_millis
-         ? options_.interval_millis
-         : config_.interval_millis;
+void Speedtest::OverrideConfigWithOptions(Config *config,
+                                          const Options &options) {
+  if (options_.num_downloads > 0) {
+    config->num_downloads = options_.num_downloads;
+  }
+  if (options_.download_bytes > 0) {
+    config->download_bytes = options_.download_bytes;
+  }
+  if (options_.num_uploads > 0) {
+    config->num_uploads = options_.num_uploads;
+  }
+  if (options_.upload_bytes > 0) {
+    config->upload_bytes = options_.upload_bytes;
+  }
+  if (options_.ping_runtime_millis > 0) {
+    config->ping_runtime_millis = options_.ping_runtime_millis;
+  }
+  if (options_.ping_timeout_millis > 0) {
+    config->ping_timeout_millis = options_.ping_timeout_millis;
+  }
+  if (options_.min_transfer_runtime > 0) {
+    config->min_transfer_runtime = options_.min_transfer_runtime;
+  }
+  if (options_.max_transfer_runtime > 0) {
+    config->max_transfer_runtime = options_.max_transfer_runtime;
+  }
+  if (options_.min_transfer_intervals > 0) {
+    config->min_transfer_intervals = options_.min_transfer_intervals;
+  }
+  if (options_.max_transfer_intervals > 0) {
+    config->max_transfer_intervals = options_.max_transfer_intervals;
+  }
+  if (options_.max_transfer_variance > 0) {
+    config->max_transfer_variance = options_.max_transfer_variance;
+  }
+  if (options_.interval_millis > 0) {
+    config->interval_millis = options_.interval_millis;
+  }
+  if (options_.exponential_moving_average){
+    config->average_type = "EXPONENTIAL";
+  }
 }
 
 http::Request::Ptr Speedtest::MakeRequest(const http::Url &url) const {
diff --git a/speedtest/speedtest.h b/speedtest/speedtest.h
index 6824dd5..7b58122 100644
--- a/speedtest/speedtest.h
+++ b/speedtest/speedtest.h
@@ -59,18 +59,7 @@
   TransferResult RunUploadTest(std::atomic_bool *cancel);
   Ping::Result RunPingTest(std::atomic_bool *cancel);
 
-  int GetNumDownloads() const;
-  long GetDownloadSizeBytes() const;
-  int GetNumUploads() const;
-  long GetUploadSizeBytes() const;
-  long GetPingTimeoutMillis() const;
-  long GetPingRunTimeMillis() const;
-  long GetMinTransferRunTimeMillis() const;
-  long GetMaxTransferRunTimeMillis() const;
-  int GetMinTransferIntervals() const;
-  int GetMaxTransferIntervals() const;
-  double GetMaxTransferVariance() const;
-  long GetIntervalMillis() const;
+  void OverrideConfigWithOptions(Config *config, const Options &options);
 
   http::Request::Ptr MakeRequest(const http::Url &url) const;
   http::Request::Ptr MakeBaseRequest(int id, const std::string &path) const;
diff --git a/speedtest/transfer_runner.h b/speedtest/transfer_runner.h
index 85bee47..4fb0620 100644
--- a/speedtest/transfer_runner.h
+++ b/speedtest/transfer_runner.h
@@ -121,6 +121,7 @@
         Bucket &bucket = result.buckets.back();
         bucket.start_time = running_time;
         bucket.total_bytes = fn.get().bytes_transferred();
+        result.total_bytes = bucket.total_bytes;
         if (options.exponential_moving_average) {
           bucket.short_megabits = GetShortEma(&result.buckets,
                                               options.min_intervals);