chromium/ash/metrics/stylus_metrics_recorder_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 "ash/metrics/stylus_metrics_recorder.h"

#include <memory>
#include <set>
#include <string>

#include "ash/system/power/peripheral_battery_listener.h"
#include "ash/system/power/peripheral_battery_tests.h"
#include "ash/test/ash_test_base.h"
#include "base/containers/contains.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "testing/gmock/include/gmock/gmock.h"

using BatteryInfo = ash::PeripheralBatteryListener::BatteryInfo;

namespace ash {
namespace {

const char kHistogramGarageSessionMetric[] =
    "ChromeOS.FeatureUsage.StylusDetachedFromGarageSession";
const char kHistogramGarageSessionUsetimeMetric[] =
    "ChromeOS.FeatureUsage.StylusDetachedFromGarageSession.Usetime";
const char kHistogramDockSessionMetric[] =
    "ChromeOS.FeatureUsage.StylusDetachedFromDockSession";
const char kHistogramDockSessionUsetimeMetric[] =
    "ChromeOS.FeatureUsage.StylusDetachedFromDockSession.Usetime";
const char kHistogramGarageOrDockSessionMetric[] =
    "ChromeOS.FeatureUsage.StylusDetachedFromGarageOrDockSession";
const char kHistogramGarageOrDockSessionUsetimeMetric[] =
    "ChromeOS.FeatureUsage.StylusDetachedFromGarageOrDockSession.Usetime";

enum class StylusChargingStyle { kDock, kGarage };

std::string BatteryKey(StylusChargingStyle style) {
  return (style == StylusChargingStyle::kGarage) ? "garaged-stylus-charger"
                                                 : "docked-stylus-charger";
}

// Test fixture for the StylusMetricsRecorder class.
class StylusMetricsRecorderTest : public AshTestBase {
 public:
  StylusMetricsRecorderTest()
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  StylusMetricsRecorderTest(const StylusMetricsRecorderTest&) = delete;
  StylusMetricsRecorderTest& operator=(const StylusMetricsRecorderTest&) =
      delete;
  ~StylusMetricsRecorderTest() override = default;

  // AshTestBase:

  void SetUp() override {
    AshTestBase::SetUp();
    stylus_metrics_recorder_ = std::make_unique<ash::StylusMetricsRecorder>();
    histogram_tester_ = std::make_unique<base::HistogramTester>();
  }

  void TearDown() override {
    stylus_metrics_recorder_.reset();
    AshTestBase::TearDown();
  }

  base::TimeTicks NowTicks() { return task_environment()->NowTicks(); }

  void AdvanceClock(base::TimeDelta delta) {
    task_environment()->AdvanceClock(delta);
  }

  BatteryInfo ConstructBatteryInfo(
      StylusChargingStyle style,
      BatteryInfo::ChargeStatus charge_status,
      bool battery_report_eligible = true,
      BatteryInfo::PeripheralType type =
          BatteryInfo::PeripheralType::kStylusViaCharger) {
    const std::string key = BatteryKey(style);
    const int level = 50;
    const std::u16string name = base::ASCIIToUTF16("name:" + key);
    const std::string btaddr = "";

    return BatteryInfo(key, name, level, battery_report_eligible, NowTicks(),
                       type, charge_status, btaddr);
  }

  void SetChargerState(StylusChargingStyle style,
                       BatteryInfo::ChargeStatus charge_status,
                       bool battery_report_eligible = true,
                       BatteryInfo::PeripheralType type =
                           BatteryInfo::PeripheralType::kStylusViaCharger) {
    const BatteryInfo info = ConstructBatteryInfo(
        style, charge_status, battery_report_eligible, type);
    if (!base::Contains(known_batteries_, info.key)) {
      stylus_metrics_recorder_->OnAddingBattery(info);
      known_batteries_.insert(info.key);
    }
    stylus_metrics_recorder_->OnUpdatedBatteryLevel(info);
  }

  void RemoveCharger(StylusChargingStyle style,
                     BatteryInfo::ChargeStatus charge_status,
                     bool battery_report_eligible = true,
                     BatteryInfo::PeripheralType type =
                         BatteryInfo::PeripheralType::kStylusViaCharger) {
    const BatteryInfo info = ConstructBatteryInfo(
        style, charge_status, battery_report_eligible, type);
    if (base::Contains(known_batteries_, info.key)) {
      stylus_metrics_recorder_->OnRemovingBattery(info);
      known_batteries_.erase(info.key);
    }
  }

 protected:
  // The test target.
  std::unique_ptr<StylusMetricsRecorder> stylus_metrics_recorder_;

  // Used to verify recorded data.
  std::unique_ptr<base::HistogramTester> histogram_tester_;

  // Track whether batteries are known or need to be added.
  std::set<std::string> known_batteries_;
};

}  // namespace

// Verifies that histogram is not recorded when no events are received.
TEST_F(StylusMetricsRecorderTest, Baseline) {
  histogram_tester_->ExpectTotalCount(kHistogramGarageSessionMetric, 0);
  histogram_tester_->ExpectTotalCount(kHistogramDockSessionMetric, 0);
  histogram_tester_->ExpectTotalCount(
      kHistogramGarageOrDockSessionUsetimeMetric, 0);
  histogram_tester_->ExpectTotalCount(kHistogramGarageSessionUsetimeMetric, 0);
  histogram_tester_->ExpectTotalCount(kHistogramDockSessionUsetimeMetric, 0);
  histogram_tester_->ExpectTotalCount(
      kHistogramGarageOrDockSessionUsetimeMetric, 0);
}

TEST_F(StylusMetricsRecorderTest, BaselineStayInGarage) {
  const base::TimeDelta kTimeSpentCharging = base::Minutes(5);

  SetChargerState(StylusChargingStyle::kGarage,
                  BatteryInfo::ChargeStatus::kCharging);
  AdvanceClock(kTimeSpentCharging);
  // By removing the battery, we force the stylus_metrics_recorder to close out
  // the session.
  RemoveCharger(StylusChargingStyle::kGarage,
                BatteryInfo::ChargeStatus::kCharging);

  histogram_tester_->ExpectTotalCount(kHistogramGarageSessionMetric, 0);
  histogram_tester_->ExpectTotalCount(kHistogramDockSessionMetric, 0);
  histogram_tester_->ExpectTotalCount(kHistogramGarageOrDockSessionMetric, 0);

  histogram_tester_->ExpectTotalCount(kHistogramGarageSessionUsetimeMetric, 0);
  histogram_tester_->ExpectTotalCount(kHistogramDockSessionUsetimeMetric, 0);
  histogram_tester_->ExpectTotalCount(
      kHistogramGarageOrDockSessionUsetimeMetric, 0);
}

TEST_F(StylusMetricsRecorderTest, BaselineStayInDock) {
  const base::TimeDelta kTimeSpentCharging = base::Minutes(5);

  SetChargerState(StylusChargingStyle::kDock,
                  BatteryInfo::ChargeStatus::kCharging);
  AdvanceClock(kTimeSpentCharging);
  // By removing the battery, we force the stylus_metrics_recorder to close out
  // the session.
  RemoveCharger(StylusChargingStyle::kDock,
                BatteryInfo::ChargeStatus::kCharging);

  histogram_tester_->ExpectTotalCount(kHistogramGarageSessionMetric, 0);
  histogram_tester_->ExpectTotalCount(kHistogramDockSessionMetric, 0);
  histogram_tester_->ExpectTotalCount(kHistogramGarageOrDockSessionMetric, 0);

  histogram_tester_->ExpectTotalCount(kHistogramGarageSessionUsetimeMetric, 0);
  histogram_tester_->ExpectTotalCount(kHistogramDockSessionUsetimeMetric, 0);
  histogram_tester_->ExpectTotalCount(
      kHistogramGarageOrDockSessionUsetimeMetric, 0);
}

TEST_F(StylusMetricsRecorderTest, RemovedFromGarage) {
  const base::TimeDelta kTimeSpentInUse = base::Minutes(5);
  const base::TimeDelta kTimeSpentCharging = base::Minutes(1);

  SetChargerState(StylusChargingStyle::kGarage,
                  BatteryInfo::ChargeStatus::kDischarging);
  AdvanceClock(kTimeSpentInUse);
  // By removing the battery, we force the stylus_metrics_recorder to close out
  // the session.
  SetChargerState(StylusChargingStyle::kGarage,
                  BatteryInfo::ChargeStatus::kCharging);
  // Step time further when we're back on charge, to make sure this time is not
  // counted
  AdvanceClock(kTimeSpentCharging);
  RemoveCharger(StylusChargingStyle::kGarage,
                BatteryInfo::ChargeStatus::kCharging);

  histogram_tester_->ExpectTotalCount(kHistogramDockSessionMetric, 0);

  histogram_tester_->ExpectBucketCount(
      kHistogramGarageSessionMetric,
      static_cast<int>(
          feature_usage::FeatureUsageMetrics::Event::kUsedWithSuccess),
      1);
  histogram_tester_->ExpectBucketCount(
      kHistogramGarageSessionMetric,
      static_cast<int>(feature_usage::FeatureUsageMetrics::Event::kEnabled), 1);
  histogram_tester_->ExpectBucketCount(
      kHistogramGarageSessionMetric,
      static_cast<int>(feature_usage::FeatureUsageMetrics::Event::kEligible),
      1);

  histogram_tester_->ExpectBucketCount(
      kHistogramGarageOrDockSessionMetric,
      static_cast<int>(
          feature_usage::FeatureUsageMetrics::Event::kUsedWithSuccess),
      1);
  histogram_tester_->ExpectBucketCount(
      kHistogramGarageOrDockSessionMetric,
      static_cast<int>(feature_usage::FeatureUsageMetrics::Event::kEnabled), 1);
  histogram_tester_->ExpectBucketCount(
      kHistogramGarageOrDockSessionMetric,
      static_cast<int>(feature_usage::FeatureUsageMetrics::Event::kEligible),
      1);

  histogram_tester_->ExpectTimeBucketCount(kHistogramGarageSessionUsetimeMetric,
                                           kTimeSpentInUse, 1);
  histogram_tester_->ExpectTimeBucketCount(
      kHistogramGarageOrDockSessionUsetimeMetric, kTimeSpentInUse, 1);
}

TEST_F(StylusMetricsRecorderTest, RemovedFromDock) {
  const base::TimeDelta kTimeSpentInUse = base::Minutes(5);
  const base::TimeDelta kTimeSpentCharging = base::Minutes(1);

  SetChargerState(StylusChargingStyle::kDock,
                  BatteryInfo::ChargeStatus::kDischarging);
  AdvanceClock(kTimeSpentInUse);
  // By removing the battery, we force the stylus_metrics_recorder to close out
  // the session.
  SetChargerState(StylusChargingStyle::kDock,
                  BatteryInfo::ChargeStatus::kCharging);
  // Step time further when we're back on charge, to make sure this time is not
  // counted
  AdvanceClock(kTimeSpentCharging);
  RemoveCharger(StylusChargingStyle::kDock,
                BatteryInfo::ChargeStatus::kCharging);

  histogram_tester_->ExpectTotalCount(kHistogramGarageSessionMetric, 0);

  histogram_tester_->ExpectBucketCount(
      kHistogramDockSessionMetric,
      static_cast<int>(
          feature_usage::FeatureUsageMetrics::Event::kUsedWithSuccess),
      1);
  histogram_tester_->ExpectBucketCount(
      kHistogramDockSessionMetric,
      static_cast<int>(feature_usage::FeatureUsageMetrics::Event::kEnabled), 1);
  histogram_tester_->ExpectBucketCount(
      kHistogramDockSessionMetric,
      static_cast<int>(feature_usage::FeatureUsageMetrics::Event::kEligible),
      1);

  histogram_tester_->ExpectBucketCount(
      kHistogramGarageOrDockSessionMetric,
      static_cast<int>(
          feature_usage::FeatureUsageMetrics::Event::kUsedWithSuccess),
      1);
  histogram_tester_->ExpectBucketCount(
      kHistogramGarageOrDockSessionMetric,
      static_cast<int>(feature_usage::FeatureUsageMetrics::Event::kEnabled), 1);
  histogram_tester_->ExpectBucketCount(
      kHistogramGarageOrDockSessionMetric,
      static_cast<int>(feature_usage::FeatureUsageMetrics::Event::kEligible),
      1);

  histogram_tester_->ExpectTimeBucketCount(kHistogramDockSessionUsetimeMetric,
                                           kTimeSpentInUse, 1);
  histogram_tester_->ExpectTimeBucketCount(
      kHistogramGarageOrDockSessionUsetimeMetric, kTimeSpentInUse, 1);
}

TEST_F(StylusMetricsRecorderTest, ShutdownWhileStylusRemoved) {
  const base::TimeDelta kTimeSpentInUse = base::Minutes(5);

  SetChargerState(StylusChargingStyle::kGarage,
                  BatteryInfo::ChargeStatus::kDischarging);
  AdvanceClock(kTimeSpentInUse);
  // By removing the battery, we force the stylus_metrics_recorder to close out
  // the session.
  SetChargerState(StylusChargingStyle::kGarage,
                  BatteryInfo::ChargeStatus::kCharging);
  // Just destroy the recorder, without replacing the stylus; we should still
  // see the time recorded
  stylus_metrics_recorder_.reset();

  histogram_tester_->ExpectBucketCount(
      kHistogramGarageSessionMetric,
      static_cast<int>(
          feature_usage::FeatureUsageMetrics::Event::kUsedWithSuccess),
      1);

  histogram_tester_->ExpectTimeBucketCount(kHistogramGarageSessionUsetimeMetric,
                                           kTimeSpentInUse, 1);
}

TEST_F(StylusMetricsRecorderTest, StylusUsageOverMultipleDays) {
  const base::TimeDelta kTimeSpentInUse = base::Hours(48);

  SetChargerState(StylusChargingStyle::kGarage,
                  BatteryInfo::ChargeStatus::kDischarging);
  AdvanceClock(kTimeSpentInUse);
  RemoveCharger(StylusChargingStyle::kGarage,
                BatteryInfo::ChargeStatus::kCharging);

  histogram_tester_->ExpectBucketCount(
      kHistogramGarageSessionMetric,
      static_cast<int>(
          feature_usage::FeatureUsageMetrics::Event::kUsedWithSuccess),
      1);

  histogram_tester_->ExpectTimeBucketCount(kHistogramGarageSessionUsetimeMetric,
                                           kTimeSpentInUse, 1);
}

TEST_F(StylusMetricsRecorderTest, StylusChargeSequencing) {
  const base::TimeDelta kTimeSpentTrickleCharging = base::Minutes(1);
  const base::TimeDelta kTimeSpentCharging = base::Minutes(60);
  const base::TimeDelta kTimeSpentFull = base::Minutes(5);
  const base::TimeDelta kTimeSpentDischarging = base::Minutes(60);
  const int kCycles = 2;

  // Initial state, stylus is garage, charging, not in use
  SetChargerState(StylusChargingStyle::kGarage,
                  BatteryInfo::ChargeStatus::kCharging);

  for (int cycle = 0; cycle < kCycles; cycle++) {
    SetChargerState(StylusChargingStyle::kGarage,
                    BatteryInfo::ChargeStatus::kCharging);
    AdvanceClock(kTimeSpentTrickleCharging);
    SetChargerState(StylusChargingStyle::kGarage,
                    BatteryInfo::ChargeStatus::kCharging);
    AdvanceClock(kTimeSpentCharging);
    SetChargerState(StylusChargingStyle::kGarage,
                    BatteryInfo::ChargeStatus::kFull);
    AdvanceClock(kTimeSpentFull);
    // Stylus is removed from garage when it starts discharging
    SetChargerState(StylusChargingStyle::kGarage,
                    BatteryInfo::ChargeStatus::kDischarging);
    AdvanceClock(kTimeSpentDischarging);
  }

  // Final state, same as initial
  SetChargerState(StylusChargingStyle::kGarage,
                  BatteryInfo::ChargeStatus::kCharging);

  histogram_tester_->ExpectBucketCount(
      kHistogramGarageSessionMetric,
      static_cast<int>(
          feature_usage::FeatureUsageMetrics::Event::kUsedWithSuccess),
      kCycles);

  histogram_tester_->ExpectTimeBucketCount(kHistogramGarageSessionUsetimeMetric,
                                           kTimeSpentDischarging, kCycles);
}

}  // namespace ash