chromium/tools/mac/power/power_sampler/battery_sampler_unittest.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "tools/mac/power/power_sampler/battery_sampler.h"

#include <cstdint>
#include <memory>

#include <IOKit/IOKitLib.h>

#include "base/time/time.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace power_sampler {

namespace {

using testing::UnorderedElementsAre;

class TestBatterySampler : public BatterySampler {
 public:
  // Make public for testing.
  using BatterySampler::BatteryData;
  using BatterySampler::MaybeComputeAvgConsumption;
  static std::unique_ptr<BatterySampler> CreateForTesting();
};

class BatterySamplerTest : public testing::Test {
 protected:
  using BatteryData = TestBatterySampler::BatteryData;

  void SetUp() override { set_battery_data(std::nullopt); }

  static void set_battery_data(std::optional<BatteryData> battery_data) {
    battery_data_ = battery_data;
  }

  static void set_seconds_since_epoch(int64_t seconds_since_epoch) {
    seconds_since_epoch_ = seconds_since_epoch;
  }

  // The gmock *ElementsAre* matchers are too exacting for the double values
  // in our samples, but this poor man's substitute will do for our needs.
  template <size_t N>
  void ExpectSampleMatchesArray(
      const Sampler::Sample& sample,
      const std::pair<std::string, double> (&datums)[N]) {
    EXPECT_EQ(N, sample.size());
    for (size_t i = 0; i < N; ++i) {
      const auto& name = datums[i].first;
      const double value = datums[i].second;

      auto it = sample.find(name);
      EXPECT_TRUE(it != sample.end()) << " for " << name;
      if (it != sample.end())
        EXPECT_DOUBLE_EQ(it->second, value) << " for " << name;
    }
  }

 private:
  friend class TestBatterySampler;

  static std::optional<BatteryData> GetStaticBatteryData(
      io_service_t power_source) {
    return battery_data_;
  }

  static int64_t GetSecondsSinceEpoch() { return seconds_since_epoch_; }

  static std::optional<BatteryData> battery_data_;
  static int64_t seconds_since_epoch_;
};

std::optional<BatterySamplerTest::BatteryData>
    BatterySamplerTest::battery_data_;

int64_t BatterySamplerTest::seconds_since_epoch_;

// static
std::unique_ptr<BatterySampler> TestBatterySampler::CreateForTesting() {
  return BatterySampler::CreateImpl(BatterySamplerTest::GetStaticBatteryData,
                                    BatterySamplerTest::GetSecondsSinceEpoch,
                                    base::mac::ScopedIOObject<io_service_t>());
}

}  // namespace

TEST_F(BatterySamplerTest, CreateFailsWhenNoData) {
  EXPECT_EQ(nullptr, TestBatterySampler::CreateForTesting());
}

TEST_F(BatterySamplerTest, CreateSucceedsWithData) {
  set_battery_data(TestBatterySampler::BatteryData{});
  EXPECT_NE(nullptr, TestBatterySampler::CreateForTesting());
}

TEST_F(BatterySamplerTest, NameAndGetDatumNameUnits) {
  set_battery_data(TestBatterySampler::BatteryData{});
  std::unique_ptr<BatterySampler> sampler(
      TestBatterySampler::CreateForTesting());
  ASSERT_NE(nullptr, sampler.get());

  EXPECT_EQ("battery", sampler->GetName());

  auto datum_name_units = sampler->GetDatumNameUnits();
  EXPECT_THAT(
      datum_name_units,
      UnorderedElementsAre(std::make_pair("external_connected", "bool"),
                           std::make_pair("voltage", "V"),
                           std::make_pair("current_capacity", "Ah"),
                           std::make_pair("max_capacity", "Ah"),
                           std::make_pair("avg_power", "W"),
                           std::make_pair("electric_charge_delta", "mAh"),
                           std::make_pair("sample_age", "s")));
}

TEST_F(BatterySamplerTest, MaybeComputeAvgConsumption) {
  TestBatterySampler::BatteryData prev_data{
      .voltage_mv = 11100,           // 11.1V.
      .current_capacity_mah = 2001,  // 2.001 Ah remaining.
      .max_capacity_mah = 5225       // Corresponds to 58Wh/11.1V in mAh.
  };
  TestBatterySampler::BatteryData new_data = prev_data;

  base::TimeDelta delta = base::Minutes(1);
  // No power if the data is identical.
  auto consumption = TestBatterySampler::MaybeComputeAvgConsumption(
      delta, prev_data, new_data);
  EXPECT_FALSE(consumption.has_value());

  // Adjust current capacity and max capacity by the same value, which means
  // net zero consumption.
  new_data.current_capacity_mah -= 51;
  new_data.max_capacity_mah -= 51;
  consumption = TestBatterySampler::MaybeComputeAvgConsumption(delta, prev_data,
                                                               new_data);
  EXPECT_FALSE(consumption.has_value());

  // Consume 1mAh.
  new_data.current_capacity_mah -= 1;
  consumption = TestBatterySampler::MaybeComputeAvgConsumption(delta, prev_data,
                                                               new_data);
  ASSERT_TRUE(consumption.has_value());
  double expected_power_w =
      (11.1 + 11.1) / 2.0 *      // Average voltage (V).
      (1.0 * 3600.0 / 1000.0) /  // Current consumption (As).
      60.0;                      // 1 minute (s).
  EXPECT_DOUBLE_EQ(expected_power_w, consumption->watts);
  EXPECT_DOUBLE_EQ(1, consumption->mah);

  // Try a voltage change.
  new_data.voltage_mv = 11200;  // 11.2V.

  // And compute the consumption over two minutes.
  consumption = TestBatterySampler::MaybeComputeAvgConsumption(
      2 * delta, prev_data, new_data);
  ASSERT_TRUE(consumption.has_value());
  expected_power_w = (11.1 + 11.2) / 2.0 *      // Average voltage (V).
                     (1.0 * 3600.0 / 1000.0) /  // Current consumption (As).
                     120.0;                     // 2 minutes (s).
  EXPECT_DOUBLE_EQ(expected_power_w, consumption->watts);
  EXPECT_DOUBLE_EQ(1, consumption->mah);
}

TEST_F(BatterySamplerTest, ReturnsSamplesAndComputesPower) {
  TestBatterySampler::BatteryData battery_data{
      .external_connected = true,
      .voltage_mv = 11100,           // 11.1V.
      .current_capacity_mah = 2001,  // 2.001 Ah remaining.
      .max_capacity_mah = 5225,      // Corresponds to 58Wh/11.1V in mAh.
      .update_time_seconds_since_epoch = 42};
  set_battery_data(battery_data);
  set_seconds_since_epoch(43);
  std::unique_ptr<BatterySampler> sampler(
      TestBatterySampler::CreateForTesting());

  ASSERT_NE(nullptr, sampler.get());
  base::TimeTicks now = base::TimeTicks::Now();

  set_battery_data(battery_data);
  Sampler::Sample datums = sampler->GetSample(now);

  // There's no power estimate for the initial sample.
  ExpectSampleMatchesArray(datums, {std::make_pair("external_connected", true),
                                    std::make_pair("voltage", 11.1),
                                    std::make_pair("current_capacity", 2.001),
                                    std::make_pair("max_capacity", 5.225),
                                    std::make_pair("sample_age", 1)});

  battery_data.current_capacity_mah -= 1;
  battery_data.update_time_seconds_since_epoch = 44;
  set_battery_data(battery_data);
  set_seconds_since_epoch(46);
  constexpr base::TimeDelta kOneMinute = base::Minutes(1);
  now += kOneMinute;
  datums = sampler->GetSample(now);
  double expected_power_w =
      (11.1 + 11.1) / 2.0 *      // Average voltage (V).
      (1.0 * 3600.0 / 1000.0) /  // Current consumption (As).
      60.0;                      // 1 minute (s).
  ExpectSampleMatchesArray(
      datums,
      {std::make_pair("external_connected", true),
       std::make_pair("voltage", 11.1), std::make_pair("current_capacity", 2),
       std::make_pair("max_capacity", 5.225),
       std::make_pair("avg_power", expected_power_w),
       std::make_pair("electric_charge_delta", 1),
       std::make_pair("sample_age", 2)});

  battery_data.voltage_mv = 11200;  // 11.2V.
  battery_data.update_time_seconds_since_epoch = 47;
  set_battery_data(battery_data);
  set_seconds_since_epoch(48);
  now += kOneMinute;
  datums = sampler->GetSample(now);
  // So long as there's no current consumption, there's no power estimate.
  ExpectSampleMatchesArray(
      datums,
      {std::make_pair("external_connected", true),
       std::make_pair("voltage", 11.2), std::make_pair("current_capacity", 2),
       std::make_pair("max_capacity", 5.225), std::make_pair("sample_age", 1)});

  battery_data.current_capacity_mah -= 1;
  battery_data.update_time_seconds_since_epoch = 49;
  set_battery_data(battery_data);
  set_seconds_since_epoch(49);
  now += kOneMinute;
  datums = sampler->GetSample(now);

  expected_power_w = (11.1 + 11.2) / 2.0 *      // Average voltage (V).
                     (1.0 * 3600.0 / 1000.0) /  // Current consumption (As).
                     120.0;                     // 2 minutes (s).
  // The above makes roughly 330mW.
  EXPECT_DOUBLE_EQ(expected_power_w, 0.3345);
  ExpectSampleMatchesArray(datums,
                           {std::make_pair("external_connected", true),
                            std::make_pair("voltage", 11.2),
                            std::make_pair("current_capacity", 1.999),
                            std::make_pair("max_capacity", 5.225),
                            std::make_pair("avg_power", expected_power_w),
                            std::make_pair("electric_charge_delta", 1),
                            std::make_pair("sample_age", 0)});
}

}  // namespace power_sampler