| // Copyright (c) 2014 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "power_manager/powerd/system/display/display_watcher.h" |
| |
| #include <string> |
| #include <vector> |
| |
| #include <base/compiler_specific.h> |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_temp_dir.h> |
| #include <gtest/gtest.h> |
| |
| #include "power_manager/powerd/system/udev_stub.h" |
| #include "power_manager/powerd/system/udev_subsystem_observer.h" |
| |
| namespace power_manager { |
| namespace system { |
| |
| namespace { |
| |
| // Stub implementation of DisplayWatcherObserver. |
| class TestObserver : public DisplayWatcherObserver { |
| public: |
| TestObserver() : num_display_changes_(0) {} |
| TestObserver(const TestObserver&) = delete; |
| TestObserver& operator=(const TestObserver&) = delete; |
| |
| virtual ~TestObserver() {} |
| |
| int num_display_changes() const { return num_display_changes_; } |
| |
| // DisplayWatcherObserver implementation: |
| void OnDisplaysChanged(const std::vector<DisplayInfo>& displays) override { |
| num_display_changes_++; |
| } |
| |
| private: |
| // Number of times that OnDisplaysChanged() has been called. |
| int num_display_changes_; |
| }; |
| |
| } // namespace |
| |
| class DisplayWatcherTest : public testing::Test { |
| public: |
| DisplayWatcherTest() { |
| CHECK(drm_dir_.CreateUniqueTempDir()); |
| watcher_.set_sysfs_drm_path_for_testing(drm_dir_.GetPath()); |
| CHECK(device_dir_.CreateUniqueTempDir()); |
| watcher_.set_i2c_dev_path_for_testing(device_dir_.GetPath()); |
| } |
| ~DisplayWatcherTest() override {} |
| |
| protected: |
| // Creates a directory named |device_name| in |device_dir_| and adds a symlink |
| // to it in |drm_dir_|. Returns the path to the directory. |
| base::FilePath CreateDrmDevice(const std::string& device_name) { |
| base::FilePath device_path = device_dir_.GetPath().Append(device_name); |
| CHECK(base::CreateDirectory(device_path)); |
| CHECK(base::CreateSymbolicLink(device_path, |
| drm_dir_.GetPath().Append(device_name))); |
| return device_path; |
| } |
| |
| // Creates a file named |device_name| in |device_dir_|. Returns the path to |
| // the file. |
| base::FilePath CreateI2CDevice(const std::string& device_name) { |
| base::FilePath device_path = device_dir_.GetPath().Append(device_name); |
| CHECK_EQ(base::WriteFile(device_path, "\n", 1), 1); |
| return device_path; |
| } |
| |
| // Notifies |watcher_| about a Udev event to trigger a rescan of displays. |
| void NotifyAboutUdevEvent() { |
| udev_.NotifySubsystemObservers( |
| {{DisplayWatcher::kDrmUdevSubsystem, "devtype", "sysname", ""}, |
| UdevEvent::Action::CHANGE}); |
| } |
| |
| // Directory with symlinks to DRM devices. |
| base::ScopedTempDir drm_dir_; |
| |
| // Directory holding device information symlinked to from the above |
| // directories. |
| base::ScopedTempDir device_dir_; |
| |
| UdevStub udev_; |
| DisplayWatcher watcher_; |
| }; |
| |
| TEST_F(DisplayWatcherTest, DisplayStatus) { |
| TestObserver observer; |
| watcher_.AddObserver(&observer); |
| watcher_.Init(&udev_); |
| EXPECT_EQ(static_cast<size_t>(0), watcher_.GetDisplays().size()); |
| |
| // Disconnected if there's no status file. |
| base::FilePath device_path = CreateDrmDevice("card0-DP-1"); |
| NotifyAboutUdevEvent(); |
| EXPECT_EQ(static_cast<size_t>(0), watcher_.GetDisplays().size()); |
| |
| // Disconnected if the status file doesn't report the connected state. |
| const char kDisconnected[] = "disconnected"; |
| base::FilePath status_path = |
| device_path.Append(DisplayWatcher::kDrmStatusFile); |
| ASSERT_TRUE( |
| base::WriteFile(status_path, kDisconnected, strlen(kDisconnected))); |
| NotifyAboutUdevEvent(); |
| EXPECT_EQ(static_cast<size_t>(0), watcher_.GetDisplays().size()); |
| |
| // Observers should be notified when the device's status goes to "unknown". |
| ASSERT_TRUE(base::WriteFile(status_path, DisplayWatcher::kDrmStatusUnknown, |
| strlen(DisplayWatcher::kDrmStatusUnknown))); |
| NotifyAboutUdevEvent(); |
| ASSERT_EQ(static_cast<size_t>(1), watcher_.GetDisplays().size()); |
| EXPECT_EQ(system::DisplayInfo::ConnectorStatus::UNKNOWN, |
| watcher_.GetDisplays().front().connector_status); |
| EXPECT_TRUE(watcher_.trigger_debounce_timeout_for_testing()); |
| EXPECT_EQ(1, observer.num_display_changes()); |
| |
| // Observers should be notified when the device's status goes to |
| // "connected" from "unknown". |
| ASSERT_TRUE(base::WriteFile(status_path, DisplayWatcher::kDrmStatusConnected, |
| strlen(DisplayWatcher::kDrmStatusConnected))); |
| NotifyAboutUdevEvent(); |
| ASSERT_EQ(static_cast<size_t>(1), watcher_.GetDisplays().size()); |
| EXPECT_EQ(system::DisplayInfo::ConnectorStatus::CONNECTED, |
| watcher_.GetDisplays().front().connector_status); |
| // Make sure observers receive a notification when the status changes from |
| // "unkown" to "connected". |
| EXPECT_TRUE(watcher_.trigger_debounce_timeout_for_testing()); |
| EXPECT_EQ(2, observer.num_display_changes()); |
| |
| // A trailing newline should be okay. |
| std::string kConnectedNewline(DisplayWatcher::kDrmStatusConnected); |
| kConnectedNewline += "\n"; |
| ASSERT_TRUE(base::WriteFile(status_path, kConnectedNewline.c_str(), |
| kConnectedNewline.size())); |
| NotifyAboutUdevEvent(); |
| ASSERT_EQ(static_cast<size_t>(1), watcher_.GetDisplays().size()); |
| EXPECT_EQ(drm_dir_.GetPath().Append(device_path.BaseName()).value(), |
| watcher_.GetDisplays()[0].drm_path.value()); |
| |
| // Add a second disconnected device. |
| base::FilePath second_device_path = CreateDrmDevice("card0-DP-0"); |
| base::FilePath second_status_path = |
| second_device_path.Append(DisplayWatcher::kDrmStatusFile); |
| ASSERT_TRUE(base::WriteFile(second_status_path, kDisconnected, |
| strlen(kDisconnected))); |
| NotifyAboutUdevEvent(); |
| ASSERT_EQ(static_cast<size_t>(1), watcher_.GetDisplays().size()); |
| EXPECT_EQ(drm_dir_.GetPath().Append(device_path.BaseName()).value(), |
| watcher_.GetDisplays()[0].drm_path.value()); |
| |
| // Connect the second device. It should be reported first since devices are |
| // sorted alphabetically. |
| ASSERT_TRUE(base::WriteFile(second_status_path, |
| DisplayWatcher::kDrmStatusConnected, |
| strlen(DisplayWatcher::kDrmStatusConnected))); |
| NotifyAboutUdevEvent(); |
| ASSERT_EQ(static_cast<size_t>(2), watcher_.GetDisplays().size()); |
| EXPECT_EQ(drm_dir_.GetPath().Append(second_device_path.BaseName()).value(), |
| watcher_.GetDisplays()[0].drm_path.value()); |
| EXPECT_EQ(drm_dir_.GetPath().Append(device_path.BaseName()).value(), |
| watcher_.GetDisplays()[1].drm_path.value()); |
| |
| // Disconnect both devices and create a new device that has a |
| // "connected" status but doesn't match the expected naming pattern for a |
| // video card. |
| ASSERT_TRUE( |
| base::WriteFile(status_path, kDisconnected, strlen(kDisconnected))); |
| ASSERT_TRUE(base::WriteFile(second_status_path, kDisconnected, |
| strlen(kDisconnected))); |
| base::FilePath misnamed_device_path = CreateDrmDevice("control32"); |
| base::FilePath misnamed_status_path = |
| misnamed_device_path.Append(DisplayWatcher::kDrmStatusFile); |
| ASSERT_TRUE(base::WriteFile(misnamed_status_path, kConnectedNewline.c_str(), |
| kConnectedNewline.size())); |
| NotifyAboutUdevEvent(); |
| EXPECT_EQ(static_cast<size_t>(0), watcher_.GetDisplays().size()); |
| } |
| |
| TEST_F(DisplayWatcherTest, I2CDevices) { |
| // Create a single connected device with no I2C device. |
| base::FilePath device_path = CreateDrmDevice("card0-DP-1"); |
| base::FilePath status_path = |
| device_path.Append(DisplayWatcher::kDrmStatusFile); |
| ASSERT_TRUE(base::WriteFile(status_path, DisplayWatcher::kDrmStatusConnected, |
| strlen(DisplayWatcher::kDrmStatusConnected))); |
| |
| watcher_.Init(&udev_); |
| ASSERT_EQ(static_cast<size_t>(1), watcher_.GetDisplays().size()); |
| EXPECT_EQ("", watcher_.GetDisplays()[0].i2c_path.value()); |
| |
| // Create an I2C directory within the DRM directory and check that the I2C |
| // device's path is set. |
| const char kI2CName[] = "i2c-3"; |
| base::FilePath i2c_path = CreateI2CDevice(kI2CName); |
| base::FilePath drm_i2c_path = device_path.Append(kI2CName); |
| ASSERT_TRUE(base::CreateDirectory(drm_i2c_path)); |
| NotifyAboutUdevEvent(); |
| ASSERT_EQ(static_cast<size_t>(1), watcher_.GetDisplays().size()); |
| EXPECT_EQ(i2c_path.value(), watcher_.GetDisplays()[0].i2c_path.value()); |
| |
| // If the I2C device doesn't actually exist, the path shouldn't be set. |
| ASSERT_TRUE(base::DeleteFile(i2c_path)); |
| NotifyAboutUdevEvent(); |
| ASSERT_EQ(static_cast<size_t>(1), watcher_.GetDisplays().size()); |
| EXPECT_EQ("", watcher_.GetDisplays()[0].i2c_path.value()); |
| |
| // Create a device with a bogus name and check that it doesn't get returned. |
| const char kBogusName[] = "i3c-1"; |
| base::FilePath bogus_path = CreateI2CDevice(kBogusName); |
| ASSERT_TRUE(base::CreateDirectory(device_path.Append(kBogusName))); |
| ASSERT_TRUE(base::DeleteFile(drm_i2c_path)); |
| NotifyAboutUdevEvent(); |
| ASSERT_EQ(static_cast<size_t>(1), watcher_.GetDisplays().size()); |
| EXPECT_EQ("", watcher_.GetDisplays()[0].i2c_path.value()); |
| } |
| |
| TEST_F(DisplayWatcherTest, Observer) { |
| // The observer shouldn't be notified when Init() is called without any |
| // displays present. |
| TestObserver observer; |
| watcher_.AddObserver(&observer); |
| watcher_.Init(&udev_); |
| EXPECT_FALSE(watcher_.trigger_debounce_timeout_for_testing()); |
| EXPECT_EQ(0, observer.num_display_changes()); |
| |
| // It also shouldn't be notified in response to a Udev event if nothing |
| // changed. |
| NotifyAboutUdevEvent(); |
| EXPECT_FALSE(watcher_.trigger_debounce_timeout_for_testing()); |
| EXPECT_EQ(0, observer.num_display_changes()); |
| |
| // After adding a display, the observer should be notified. |
| base::FilePath device_path = CreateDrmDevice("card0-DP-1"); |
| base::FilePath status_path = |
| device_path.Append(DisplayWatcher::kDrmStatusFile); |
| ASSERT_TRUE(base::WriteFile(status_path, DisplayWatcher::kDrmStatusConnected, |
| strlen(DisplayWatcher::kDrmStatusConnected))); |
| NotifyAboutUdevEvent(); |
| EXPECT_TRUE(watcher_.trigger_debounce_timeout_for_testing()); |
| EXPECT_EQ(1, observer.num_display_changes()); |
| |
| // It shouldn't be notified for another no-op Udev event. |
| NotifyAboutUdevEvent(); |
| EXPECT_FALSE(watcher_.trigger_debounce_timeout_for_testing()); |
| EXPECT_EQ(1, observer.num_display_changes()); |
| |
| // After the device is disconnected, the observer should be notified one more |
| // time. |
| ASSERT_TRUE(base::DeleteFile(status_path)); |
| NotifyAboutUdevEvent(); |
| EXPECT_TRUE(watcher_.trigger_debounce_timeout_for_testing()); |
| EXPECT_EQ(2, observer.num_display_changes()); |
| |
| watcher_.RemoveObserver(&observer); |
| } |
| |
| TEST_F(DisplayWatcherTest, DebounceTimer) { |
| TestObserver observer; |
| watcher_.AddObserver(&observer); |
| watcher_.Init(&udev_); |
| |
| // After adding a display, the observer should be not be notified before |
| // debounce timer expires. |
| base::FilePath device_path = CreateDrmDevice("card0-DP-1"); |
| base::FilePath status_path = |
| device_path.Append(DisplayWatcher::kDrmStatusFile); |
| ASSERT_TRUE(base::WriteFile(status_path, DisplayWatcher::kDrmStatusConnected, |
| strlen(DisplayWatcher::kDrmStatusConnected))); |
| NotifyAboutUdevEvent(); |
| EXPECT_EQ(0, observer.num_display_changes()); |
| // But on timer expiry, observer should be notified. |
| EXPECT_TRUE(watcher_.trigger_debounce_timeout_for_testing()); |
| EXPECT_EQ(1, observer.num_display_changes()); |
| |
| watcher_.RemoveObserver(&observer); |
| } |
| |
| } // namespace system |
| } // namespace power_manager |