chromium/chrome/browser/ash/chromebox_for_meetings/xu_camera/xu_camera_service_unittest.cc

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

#include "chrome/browser/ash/chromebox_for_meetings/xu_camera/xu_camera_service.h"

#include <asm-generic/errno.h>
#include <linux/uvcvideo.h>
#include <linux/videodev2.h>

#include <cstdint>
#include <optional>

#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
#include "chromeos/ash/components/dbus/chromebox_for_meetings/fake_cfm_hotline_client.h"
#include "chromeos/services/chromebox_for_meetings/public/cpp/fake_service_connection.h"
#include "chromeos/services/chromebox_for_meetings/public/cpp/fake_service_context.h"
#include "chromeos/services/chromebox_for_meetings/public/cpp/service_connection.h"
#include "chromeos/services/chromebox_for_meetings/public/mojom/xu_camera.mojom.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_utils.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"

using chromeos::IpPeripheralServiceClient;

namespace ash::cfm {
namespace {

const std::vector<uint8_t> kGuid() {
  return std::vector<uint8_t>({0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34,
                               0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34});
}
auto kMenuEntries() {
  return mojom::MenuEntries::New();
}
const std::vector<uint8_t> kEmpty() {
  return std::vector<uint8_t>();
}
const std::vector<uint8_t> kName() {
  return std::vector<uint8_t>(32, 'a');
}
const std::vector<uint8_t> kData() {
  return std::vector<uint8_t>({0x43, 0x21});
}
const std::vector<uint8_t> kLen() {
  return std::vector<uint8_t>({0x02, 0x00});
}                            // little-endian uint16
const int32_t kValue = 123;  // Fake v4l2 value
const std::vector<uint8_t> kValueAsUint8() {
  return std::vector<uint8_t>((std::uint8_t*)&(kValue),
                              (std::uint8_t*)&(kValue) + sizeof(std::int32_t));
}

mojom::WebcamIdPtr kDevPath() {
  return mojom::WebcamId::NewDevPath("/fake/dev/path");
}
mojom::WebcamIdPtr kIPAddr() {
  return mojom::WebcamId::NewDevPath("192.168.19.224");
}
mojom::WebcamIdPtr kDevId() {
  return mojom::WebcamId::NewDeviceId("123");
}
mojom::CtrlTypePtr kQueryCtrl() {
  return mojom::CtrlType::NewQueryCtrl(mojom::ControlQuery::New(1, 1));
}
mojom::CtrlTypePtr kCtrlMapping() {
  return mojom::CtrlType::NewMappingCtrl(mojom::ControlMapping::New(
      /* id= */ 1,
      /* name= */ kName(),
      /* guid= */ kGuid(),
      /* selector= */ 1,
      /* size= */ 1,
      /* offset= */ 1,
      /* v4l2_type= */ V4L2_CTRL_TYPE_INTEGER,
      /* data_type= */ UVC_CTRL_DATA_TYPE_SIGNED,
      /* menu_entries= */ kMenuEntries()->Clone()));
}

class TestDelegate : public XuCameraService::Delegate {
 public:
  int Ioctl(const base::ScopedFD& fd,
            unsigned int request,
            void* query) override {
    if (VIDIOC_G_CTRL == request) {
      struct v4l2_control* control = static_cast<v4l2_control*>(query);
      control->value = kValue;
    } else if (UVCIOC_CTRL_QUERY == request) {
      uvc_xu_control_query* control_query =
          static_cast<uvc_xu_control_query*>(query);
      if (UVC_GET_LEN == control_query->query) {
        control_query->data[0] = kLen()[0];
        control_query->data[1] = kLen()[1];
      } else if (UVC_GET_CUR == control_query->query) {
        control_query->data[0] = kData()[0];
        control_query->data[1] = kData()[1];
      }
    }
    return 0;
  }

  bool OpenFile(base::ScopedFD& fd, const std::string& path) override {
    if (path.empty()) {
      LOG(ERROR) << "Filepath is empty";
      return false;
    }
    // Return fake fd for unit tests.
    fd = base::ScopedFD(dup(STDERR_FILENO));
    return true;
  }
};

class CfMXuCameraServiceTest
    : public testing::TestWithParam<struct XuTestCase> {
 public:
  CfMXuCameraServiceTest() = default;
  CfMXuCameraServiceTest(const CfMXuCameraServiceTest&) = delete;
  CfMXuCameraServiceTest& operator=(const CfMXuCameraServiceTest&) = delete;

  void SetUp() override {
    IpPeripheralServiceClient::InitializeFake();

    CfmHotlineClient::InitializeFake();
    chromeos::cfm::ServiceConnection::UseFakeServiceConnectionForTesting(
        &fake_service_connection_);
    XuCameraService::InitializeForTesting(new TestDelegate());
  }

  void TearDown() override {
    XuCameraService::Shutdown();
    CfmHotlineClient::Shutdown();
    IpPeripheralServiceClient::Shutdown();
  }

  FakeCfmHotlineClient* GetClient() {
    return static_cast<FakeCfmHotlineClient*>(CfmHotlineClient::Get());
  }

  // Returns a mojo::Remote for the mojom::XuCamera by faking the
  // way the cfm mojom binder daemon would request it through chrome.
  const mojo::Remote<mojom::XuCamera>& GetXuCameraRemote() {
    if (!XuCameraService::IsInitialized()) {
      XuCameraService::InitializeForTesting(&delegate_);
    }
    if (xu_camera_remote_.is_bound()) {
      return xu_camera_remote_;
    }

    // if there is no valid remote create one
    auto* interface_name = mojom::XuCamera::Name_;

    base::RunLoop run_loop;

    // Fake out CfmServiceContext
    fake_service_connection_.SetCallback(base::BindLambdaForTesting(
        [&](mojo::PendingReceiver<chromeos::cfm::mojom::CfmServiceContext>
                pending_receiver,
            bool success) {
          ASSERT_TRUE(success);
          context_receiver_set_.Add(&context_, std::move(pending_receiver));
        }));

    context_.SetFakeProvideAdaptorCallback(base::BindLambdaForTesting(
        [&](const std::string& service_id,
            mojo::PendingRemote<chromeos::cfm::mojom::CfmServiceAdaptor>
                pending_adaptor_remote,
            chromeos::cfm::mojom::CfmServiceContext::ProvideAdaptorCallback
                callback) {
          ASSERT_EQ(interface_name, service_id);
          adaptor_remote_.Bind(std::move(pending_adaptor_remote));
          std::move(callback).Run(true);
        }));

    EXPECT_TRUE(GetClient()->FakeEmitSignal(interface_name));
    run_loop.RunUntilIdle();

    EXPECT_TRUE(adaptor_remote_.is_connected());

    adaptor_remote_->OnBindService(
        xu_camera_remote_.BindNewPipeAndPassReceiver().PassPipe());
    EXPECT_TRUE(xu_camera_remote_.is_connected());

    return xu_camera_remote_;
  }

 protected:
  chromeos::cfm::FakeCfmServiceContext context_;
  mojo::Remote<mojom::XuCamera> xu_camera_remote_;
  mojo::ReceiverSet<chromeos::cfm::mojom::CfmServiceContext>
      context_receiver_set_;
  mojo::Remote<chromeos::cfm::mojom::CfmServiceAdaptor> adaptor_remote_;
  chromeos::cfm::FakeServiceConnectionImpl fake_service_connection_;
  content::BrowserTaskEnvironment task_environment_;
  TestDelegate delegate_;
};

// This test ensures that the XuCameraService is discoverable by its
// mojom name by sending a signal received by CfmHotlineClient.
TEST_F(CfMXuCameraServiceTest, XuCameraServiceAvailable) {
  ASSERT_TRUE(GetClient()->FakeEmitSignal(mojom::XuCamera::Name_));
}

// This test ensures that the XuCameraService correctly registers itself
// for discovery by the cfm mojom binder daemon and correctly returns a
// working mojom remote.
TEST_F(CfMXuCameraServiceTest, GetXuCameraRemote) {
  ASSERT_TRUE(GetXuCameraRemote().is_connected());
}

// This test ensure that the XU camera can get unit id
TEST_F(CfMXuCameraServiceTest, GetXuCameraUnitId) {
  base::RunLoop run_loop;
  GetXuCameraRemote()->GetUnitId(
      /* id= */ kDevPath().Clone(), /* guid= */ kGuid(),
      base::BindLambdaForTesting(
          [&](const uint8_t error_code, const uint8_t unit_id) {
            EXPECT_EQ(error_code, ENOSYS);
            EXPECT_EQ(unit_id, '0');
            run_loop.Quit();
          }));
  run_loop.Run();
}

TEST_F(CfMXuCameraServiceTest, GetXuCameraUnitIdIPCamera) {
  base::RunLoop run_loop;
  GetXuCameraRemote()->GetUnitId(
      /* id= */ kIPAddr().Clone(), /* guid= */ kGuid(),
      base::BindLambdaForTesting(
          [&](const uint8_t error_code, const uint8_t unit_id) {
            EXPECT_EQ(error_code, 0);
            EXPECT_EQ(unit_id, 0);
            run_loop.Quit();
          }));
  run_loop.Run();
}

// This test ensure that the XU camera can map control
TEST_F(CfMXuCameraServiceTest, GetXuCameraMapCtrl) {
  base::RunLoop run_loop;
  GetXuCameraRemote()->MapCtrl(
      /* id= */ kDevPath().Clone(),
      /* mapping_ctrl= */ kCtrlMapping()->get_mapping_ctrl().Clone(),
      base::BindLambdaForTesting([&](const uint8_t error_code) {
        EXPECT_EQ(error_code, 0);
        run_loop.Quit();
      }));
  run_loop.Run();
}

// This test ensure that the XU camera can get control length given a ctrl
// query
TEST_F(CfMXuCameraServiceTest, XuCameraGetCtrlLenWithDevPathCtrlQuery) {
  base::RunLoop run_loop;
  GetXuCameraRemote()->GetCtrl(
      /* id= */ kDevPath().Clone(), /* ctrl= */ kQueryCtrl().Clone(),
      /* fn= */ mojom::GetFn::kLen,
      base::BindLambdaForTesting(
          [&](const uint8_t error_code, const std::vector<uint8_t>& data) {
            EXPECT_EQ(error_code, 0);
            EXPECT_EQ(data, kLen());
            run_loop.Quit();
          }));
  run_loop.Run();
}

struct XuTestCase {
  XuTestCase(std::string test_name,
             mojom::WebcamIdPtr webcam_id,
             mojom::CtrlTypePtr ctrl_type,
             uint8_t expected_error_code,
             std::vector<uint8_t> expected_data)
      : test_name(test_name),
        webcam_id(std::move(webcam_id)),
        ctrl_type(std::move(ctrl_type)),
        expected_error_code(expected_error_code),
        expected_data(expected_data) {}
  XuTestCase(const XuTestCase& other)
      : test_name(other.test_name),
        webcam_id(other.webcam_id.Clone()),
        ctrl_type(other.ctrl_type.Clone()),
        expected_error_code(other.expected_error_code),
        expected_data(other.expected_data) {}
  std::string test_name;
  const mojom::WebcamIdPtr webcam_id;
  const mojom::CtrlTypePtr ctrl_type;
  uint8_t expected_error_code;
  std::vector<uint8_t> expected_data;  // used only for GetCtrl tests
};

const XuTestCase xu_test_cases[] = {
    {"DevPath_CtrlQuery", kDevPath(), kQueryCtrl(), 0, kData()},
    {"DevId_CtrlQuery", kDevId(), kQueryCtrl(), ENOENT, kEmpty()},
    {"DevPath_CtrlMapping", kDevPath(), kCtrlMapping(), 0, kValueAsUint8()},
    {"DevId_CtrlMapping", kDevId(), kCtrlMapping(), ENOENT, kEmpty()},
    {"IPAddr_CtrlQuery", kIPAddr(), kQueryCtrl(), 0, kEmpty()},
    {"IPAddr_CtrlMapping", kIPAddr(), kCtrlMapping(), 0, kValueAsUint8()},
};

// Test that the XU camera can get control given a ctrl query/mapping
TEST_P(CfMXuCameraServiceTest, XuCameraGetCtrl) {
  base::RunLoop run_loop;
  const XuTestCase& param = GetParam();
  GetXuCameraRemote()->GetCtrl(
      /* id= */ param.webcam_id.Clone(), /* ctrl= */ param.ctrl_type.Clone(),
      /* fn= */ mojom::GetFn::kCur,
      base::BindLambdaForTesting(
          [&](const uint8_t error_code, const std::vector<uint8_t>& data) {
            EXPECT_EQ(error_code, param.expected_error_code);
            EXPECT_EQ(data, param.expected_data);
            run_loop.Quit();
          }));
  run_loop.Run();
}

// Test that the XU camera can set control given a ctrl query/mapping
TEST_P(CfMXuCameraServiceTest, XuCameraSetCtrl) {
  base::RunLoop run_loop;
  const XuTestCase& param = GetParam();
  std::vector<uint8_t> data{'a', 'b', 'c'};
  GetXuCameraRemote()->SetCtrl(
      /* id= */ param.webcam_id.Clone(), /* ctrl= */ param.ctrl_type.Clone(),
      /* data= */ data,
      base::BindLambdaForTesting([&](const uint8_t error_code) {
        EXPECT_EQ(error_code, param.expected_error_code);
        run_loop.Quit();
      }));
  run_loop.Run();
}

INSTANTIATE_TEST_SUITE_P(
    CfMXuCameraServiceTests,
    CfMXuCameraServiceTest,
    testing::ValuesIn<XuTestCase>(xu_test_cases),
    [](const testing::TestParamInfo<CfMXuCameraServiceTest::ParamType>& info) {
      return info.param.test_name;
    });

}  // namespace
}  // namespace ash::cfm