chromium/services/device/generic_sensor/platform_sensor_chromeos_unittest.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "services/device/generic_sensor/platform_sensor_chromeos.h"

#include <memory>
#include <utility>

#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "chromeos/components/sensors/fake_sensor_device.h"
#include "services/device/generic_sensor/fake_platform_sensor_and_provider.h"
#include "services/device/generic_sensor/platform_sensor_util.h"
#include "services/device/public/cpp/generic_sensor/sensor_traits.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace device {

namespace {

constexpr int kFakeDeviceId = 1;

constexpr char kAccelerometerChannels[][10] = {"accel_x", "accel_y", "accel_z"};
constexpr char kGyroscopeChannels[][10] = {"anglvel_x", "anglvel_y",
                                           "anglvel_z"};
constexpr char kMagnetometerChannels[][10] = {"magn_x", "magn_y", "magn_z"};
constexpr char kGravityChannels[][10] = {"gravity_x", "gravity_y", "gravity_z"};

constexpr double kScaleValueLightSensor = device::kAlsRoundingMultiple;

constexpr double kScaleValue = 10.0;

// The number of axes for which there are accelerometer readings.
constexpr uint32_t kNumberOfAxes = 3u;

constexpr int64_t kFakeSampleData = 1;
constexpr int64_t kFakeAxesSampleData[] = {1, 2, 3};
constexpr int64_t kFakeTimestampData = 163176689212344ll;

}  // namespace

class PlatformSensorChromeOSTestBase {
 protected:
  void SetUpBase() {
    provider_ = std::make_unique<FakePlatformSensorProvider>();

    pending_receiver_ = sensor_device_remote_.BindNewPipeAndPassReceiver();
  }

  void InitSensorDevice(
      std::vector<chromeos::sensors::FakeSensorDevice::ChannelData>
          channels_data) {
    sensor_device_ = std::make_unique<chromeos::sensors::FakeSensorDevice>(
        std::move(channels_data));
    receiver_id_ = sensor_device_->AddReceiver(std::move(pending_receiver_));
  }

  void DisableFirstChannel() {
    DCHECK(sensor_device_.get());

    sensor_device_->SetChannelsEnabledWithId(receiver_id_, {0}, false);
  }

  void OnSensorDeviceDisconnect(uint32_t custom_reason_code,
                                const std::string& description) {
    custom_reason_code_ = custom_reason_code;
  }

  std::unique_ptr<chromeos::sensors::FakeSensorDevice> sensor_device_;
  std::unique_ptr<FakePlatformSensorProvider> provider_;
  scoped_refptr<PlatformSensorChromeOS> sensor_;

  mojo::ReceiverId receiver_id_;
  mojo::Remote<chromeos::sensors::mojom::SensorDevice> sensor_device_remote_;
  mojo::PendingReceiver<chromeos::sensors::mojom::SensorDevice>
      pending_receiver_;

  std::optional<uint32_t> custom_reason_code_;

  base::test::SingleThreadTaskEnvironment task_environment;
};

class PlatformSensorChromeOSOneChannelTest
    : public PlatformSensorChromeOSTestBase,
      public ::testing::TestWithParam<
          std::pair<mojom::SensorType, const char*>> {
 protected:
  void SetUp() override {
    SetUpBase();

    auto type = GetParam().first;

    sensor_ = base::MakeRefCounted<PlatformSensorChromeOS>(
        kFakeDeviceId, type, provider_->GetSensorReadingBuffer(type),
        provider_->AsWeakPtr(),
        base::BindOnce(
            &PlatformSensorChromeOSOneChannelTest::OnSensorDeviceDisconnect,
            base::Unretained(this)),
        kScaleValueLightSensor, std::move(sensor_device_remote_));

    EXPECT_EQ(sensor_->GetReportingMode(),
              type == mojom::SensorType::AMBIENT_LIGHT
                  ? mojom::ReportingMode::ON_CHANGE
                  : mojom::ReportingMode::CONTINUOUS);
    EXPECT_EQ(sensor_->GetDefaultConfiguration().frequency(),
              GetSensorMaxAllowedFrequency(type));
  }

  void SetChannels(const char channel[], bool set_first_channel) {
    std::vector<chromeos::sensors::FakeSensorDevice::ChannelData> channels_data(
        set_first_channel ? 2 : 1);
    if (set_first_channel) {
      channels_data.front().id = channel;
      channels_data.front().sample_data = kFakeSampleData;
    }

    channels_data.back().id = chromeos::sensors::mojom::kTimestampChannel;
    channels_data.back().sample_data = kFakeTimestampData;

    InitSensorDevice(std::move(channels_data));
  }

  SensorReadingSingle& GetSensorReadingSingle(SensorReading& reading) {
    switch (GetParam().first) {
      case mojom::SensorType::AMBIENT_LIGHT:
        return reading.als;
      default:
        LOG(FATAL) << "Invalid type: " << GetParam().first;
    }
  }

  void GetRoundedSensorReadingSingle(SensorReadingSingle* reading_single) {
    reading_single->value = kFakeSampleData * kScaleValueLightSensor;
    reading_single->timestamp =
        base::Nanoseconds(kFakeTimestampData).InSecondsF();

    RoundIlluminanceReading(reading_single);
  }

  void WaitForAndCheckReading(
      testing::NiceMock<MockPlatformSensorClient>* client) {
    base::RunLoop loop;
    // Wait until a sample is received.
    EXPECT_CALL(*client, OnSensorReadingChanged(GetParam().first))
        .WillOnce(base::test::RunOnceClosure(loop.QuitClosure()));
    loop.Run();

    SensorReading reading;
    EXPECT_TRUE(sensor_->GetLatestReading(&reading));
    const auto& reading_single = GetSensorReadingSingle(reading);

    SensorReadingSingle rounded_reading_single;
    GetRoundedSensorReadingSingle(&rounded_reading_single);

    EXPECT_EQ(reading_single.value, rounded_reading_single.value);
    EXPECT_EQ(reading_single.timestamp, rounded_reading_single.timestamp);
  }
};

TEST_P(PlatformSensorChromeOSOneChannelTest, MissingChannels) {
  SetChannels(GetParam().second, /*set_first_channel=*/false);

  auto client = std::make_unique<testing::NiceMock<MockPlatformSensorClient>>();
  sensor_->AddClient(client.get());
  sensor_->StartListening(client.get(),
                          PlatformSensorConfiguration(
                              GetSensorMaxAllowedFrequency(GetParam().first)));
  EXPECT_TRUE(sensor_->IsActiveForTesting());

  EXPECT_CALL(*client.get(), OnSensorReadingChanged(GetParam().first)).Times(0);
  // Wait until all tasks done and no samples updated.
  base::RunLoop().RunUntilIdle();

  sensor_->RemoveClient(client.get());
}

TEST_P(PlatformSensorChromeOSOneChannelTest, GetSamples) {
  SetChannels(GetParam().second, /*set_first_channel=*/true);

  auto client = std::make_unique<testing::NiceMock<MockPlatformSensorClient>>();
  sensor_->AddClient(client.get());
  double frequency = GetSensorMaxAllowedFrequency(GetParam().first);
  sensor_->StartListening(client.get(), PlatformSensorConfiguration(frequency));
  EXPECT_TRUE(sensor_->IsActiveForTesting());

  WaitForAndCheckReading(client.get());

  sensor_->StopListening(client.get(), PlatformSensorConfiguration(frequency));
  sensor_->StartListening(client.get(), PlatformSensorConfiguration(frequency));
  WaitForAndCheckReading(client.get());

  DisableFirstChannel();

  EXPECT_CALL(*client.get(), OnSensorReadingChanged(GetParam().first)).Times(0);
  // Wait until a sample without the first channel is received.
  base::RunLoop().RunUntilIdle();
  // No reading updated.

  sensor_device_->ResetObserverRemoteWithReason(
      receiver_id_,
      chromeos::sensors::mojom::SensorDeviceDisconnectReason::DEVICE_REMOVED,
      "Device was removed");

  base::RunLoop loop;
  // Wait until the disconnect arrives at |sensor_|.
  EXPECT_CALL(*client.get(), OnSensorError())
      .WillOnce(base::test::RunOnceClosure(loop.QuitClosure()));
  loop.Run();

  EXPECT_EQ(
      custom_reason_code_,
      static_cast<uint32_t>(chromeos::sensors::mojom::
                                SensorDeviceDisconnectReason::DEVICE_REMOVED));
  sensor_->RemoveClient(client.get());
}

TEST_P(PlatformSensorChromeOSOneChannelTest, ResetOnTooManyFailures) {
  SetChannels(GetParam().second, /*set_first_channel=*/true);

  auto client = std::make_unique<testing::NiceMock<MockPlatformSensorClient>>();
  sensor_->AddClient(client.get());
  sensor_->StartListening(client.get(),
                          PlatformSensorConfiguration(
                              GetSensorMaxAllowedFrequency(GetParam().first)));
  EXPECT_TRUE(sensor_->IsActiveForTesting());

  WaitForAndCheckReading(client.get());

  EXPECT_CALL(*client.get(), OnSensorError()).Times(0);
  for (size_t i = 0;
       i < PlatformSensorChromeOS::kNumFailedReadsBeforeGivingUp - 1; ++i) {
    sensor_->OnErrorOccurred(
        chromeos::sensors::mojom::ObserverErrorType::READ_FAILED);
  }

  base::flat_map<int32_t, int64_t> sample;
  sample[0] = kFakeSampleData;
  sample[1] = kFakeTimestampData;

  for (size_t i = 0; i < PlatformSensorChromeOS::kNumRecoveryReads; ++i)
    sensor_->OnSampleUpdated(sample);

  // |num_failed_reads_| is recovered by 1.
  sensor_->OnErrorOccurred(
      chromeos::sensors::mojom::ObserverErrorType::READ_FAILED);

  EXPECT_CALL(*client.get(), OnSensorError()).Times(1);

  sensor_->OnErrorOccurred(
      chromeos::sensors::mojom::ObserverErrorType::READ_FAILED);

  sensor_->RemoveClient(client.get());
}

INSTANTIATE_TEST_SUITE_P(
    PlatformSensorChromeOSOneChannelTestRun,
    PlatformSensorChromeOSOneChannelTest,
    ::testing::Values(std::make_pair(mojom::SensorType::AMBIENT_LIGHT,
                                     chromeos::sensors::mojom::kLightChannel)));

class PlatformSensorChromeOSAxesTest
    : public PlatformSensorChromeOSTestBase,
      public ::testing::TestWithParam<
          std::pair<mojom::SensorType, const char (*)[10]>> {
 protected:
  void SetUp() override {
    SetUpBase();

    auto type = GetParam().first;

    sensor_ = base::MakeRefCounted<PlatformSensorChromeOS>(
        kFakeDeviceId, type, provider_->GetSensorReadingBuffer(type), nullptr,
        base::BindOnce(
            &PlatformSensorChromeOSAxesTest::OnSensorDeviceDisconnect,
            base::Unretained(this)),
        kScaleValue, std::move(sensor_device_remote_));

    EXPECT_EQ(sensor_->GetReportingMode(), mojom::ReportingMode::CONTINUOUS);
    EXPECT_EQ(sensor_->GetDefaultConfiguration().frequency(),
              GetSensorMaxAllowedFrequency(type));
  }

  void SetChannelsWithAxes(const char channels[][10], uint32_t num_of_axes) {
    CHECK_LE(num_of_axes, kNumberOfAxes);
    std::vector<chromeos::sensors::FakeSensorDevice::ChannelData> channels_data(
        num_of_axes + 1);
    for (uint32_t i = 0; i < num_of_axes; ++i) {
      channels_data[i].id = channels[i];
      channels_data[i].sample_data = kFakeAxesSampleData[i];
    }
    channels_data.back().id = chromeos::sensors::mojom::kTimestampChannel;
    channels_data.back().sample_data = kFakeTimestampData;

    InitSensorDevice(std::move(channels_data));
  }

  SensorReadingXYZ& GetSensorReadingXYZ(SensorReading& reading) {
    switch (GetParam().first) {
      case mojom::SensorType::ACCELEROMETER:
      case mojom::SensorType::GRAVITY:
        return reading.accel;
      case mojom::SensorType::GYROSCOPE:
        return reading.gyro;
      case mojom::SensorType::MAGNETOMETER:
        return reading.magn;
      default:
        LOG(FATAL) << "Invalid type: " << GetParam().first;
    }
  }

  void GetRoundedSensorReadingXYZ(SensorReadingXYZ* reading_xyz) {
    reading_xyz->x = kFakeAxesSampleData[0] * kScaleValue;
    reading_xyz->y = kFakeAxesSampleData[1] * kScaleValue;
    reading_xyz->z = kFakeAxesSampleData[2] * kScaleValue;
    reading_xyz->timestamp = base::Nanoseconds(kFakeTimestampData).InSecondsF();

    switch (GetParam().first) {
      case mojom::SensorType::ACCELEROMETER:
      case mojom::SensorType::GRAVITY:
        RoundAccelerometerReading(reading_xyz);
        break;
      case mojom::SensorType::GYROSCOPE:
        RoundGyroscopeReading(reading_xyz);
        break;
      case mojom::SensorType::MAGNETOMETER:
        RoundMagnetometerReading(reading_xyz);
        break;
      default:
        LOG(FATAL) << "Invalid type: " << GetParam().first;
    }
  }

  void WaitForAndCheckReading(
      testing::NiceMock<MockPlatformSensorClient>* client) {
    base::RunLoop loop;
    // Wait until a sample is received.
    EXPECT_CALL(*client, OnSensorReadingChanged(GetParam().first))
        .WillOnce(base::test::RunOnceClosure(loop.QuitClosure()));
    loop.Run();

    SensorReading reading;
    EXPECT_TRUE(sensor_->GetLatestReading(&reading));
    const auto& reading_xyz = GetSensorReadingXYZ(reading);

    SensorReadingXYZ rounded_reading_xyz;
    GetRoundedSensorReadingXYZ(&rounded_reading_xyz);

    EXPECT_EQ(reading_xyz.x, rounded_reading_xyz.x);
    EXPECT_EQ(reading_xyz.y, rounded_reading_xyz.y);
    EXPECT_EQ(reading_xyz.z, rounded_reading_xyz.z);
    EXPECT_EQ(reading_xyz.timestamp, rounded_reading_xyz.timestamp);
  }
};

TEST_P(PlatformSensorChromeOSAxesTest, MissingChannels) {
  SetChannelsWithAxes(GetParam().second, kNumberOfAxes - 1);

  auto client = std::make_unique<testing::NiceMock<MockPlatformSensorClient>>();
  sensor_->AddClient(client.get());
  sensor_->StartListening(client.get(),
                          PlatformSensorConfiguration(
                              GetSensorMaxAllowedFrequency(GetParam().first)));
  EXPECT_TRUE(sensor_->IsActiveForTesting());

  EXPECT_CALL(*client.get(), OnSensorReadingChanged(GetParam().first)).Times(0);
  // Wait until all tasks done and no samples updated.
  base::RunLoop().RunUntilIdle();

  sensor_->RemoveClient(client.get());
}

TEST_P(PlatformSensorChromeOSAxesTest, GetSamples) {
  SetChannelsWithAxes(GetParam().second, kNumberOfAxes);

  auto client = std::make_unique<testing::NiceMock<MockPlatformSensorClient>>();
  sensor_->AddClient(client.get());
  double frequency = GetSensorMaxAllowedFrequency(GetParam().first);
  sensor_->StartListening(client.get(), PlatformSensorConfiguration(frequency));
  EXPECT_TRUE(sensor_->IsActiveForTesting());

  WaitForAndCheckReading(client.get());

  sensor_->StopListening(client.get(), PlatformSensorConfiguration(frequency));
  sensor_->StartListening(client.get(), PlatformSensorConfiguration(frequency));
  WaitForAndCheckReading(client.get());

  DisableFirstChannel();

  EXPECT_CALL(*client.get(), OnSensorReadingChanged(GetParam().first)).Times(0);
  // Wait until a sample without the first channel is received.
  base::RunLoop().RunUntilIdle();
  // No reading updated.

  sensor_device_->RemoveReceiver(receiver_id_);

  base::RunLoop loop;
  // Wait until the disconnect arrives at |sensor_|.
  EXPECT_CALL(*client.get(), OnSensorError())
      .WillOnce(base::test::RunOnceClosure(loop.QuitClosure()));
  loop.Run();

  sensor_->RemoveClient(client.get());
}

INSTANTIATE_TEST_SUITE_P(
    PlatformSensorChromeOSAxesTestRun,
    PlatformSensorChromeOSAxesTest,
    ::testing::Values(
        std::make_pair(mojom::SensorType::ACCELEROMETER,
                       kAccelerometerChannels),
        std::make_pair(mojom::SensorType::GYROSCOPE, kGyroscopeChannels),
        std::make_pair(mojom::SensorType::MAGNETOMETER, kMagnetometerChannels),
        std::make_pair(mojom::SensorType::GRAVITY, kGravityChannels)));

}  // namespace device