blob: a5456829540cadd33aa5350b1e8947e0465edfd8 [file] [log] [blame]
// 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/external_display.h"
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <string>
#include <utility>
#include <base/compiler_specific.h>
#include <base/strings/string_number_conversions.h>
#include <gtest/gtest.h>
#include "power_manager/common/metrics_constants.h"
#include "power_manager/common/metrics_sender_stub.h"
namespace power_manager {
namespace system {
namespace {
// Returns a two-character hexadecimal representation of |byte|.
std::string Hex(uint8_t byte) {
return base::HexEncode(&byte, 1);
}
// Test implementation of ExternalDisplay::Delegate.
class TestDelegate : public ExternalDisplay::Delegate {
public:
TestDelegate() : report_write_failure_(false), report_read_failure_(false) {}
TestDelegate(const TestDelegate&) = delete;
TestDelegate& operator=(const TestDelegate&) = delete;
~TestDelegate() override {}
void set_reply_message(const std::vector<uint8_t>& message) {
reply_message_ = message;
}
void set_report_write_failure(bool failure) {
report_write_failure_ = failure;
}
void set_report_read_failure(bool failure) { report_read_failure_ = failure; }
// Returns the single message present in |sent_messages_|, if any, and clears
// the vector. Crashes if multiple messages are present.
std::string PopSentMessage() {
std::string message;
CHECK_LE(sent_messages_.size(), 1u);
if (!sent_messages_.empty())
message = sent_messages_[0];
sent_messages_.clear();
return message;
}
// ExternalDisplay::Delegate implementation:
std::string GetName() const override { return "i2c-test"; }
bool PerformI2COperation(struct i2c_rdwr_ioctl_data* data) override {
// Check that the passed-in data is remotely sane.
CHECK(data);
CHECK_EQ(data->nmsgs, 1u);
struct i2c_msg* const i2c_message = data->msgs;
CHECK(i2c_message);
CHECK(i2c_message->buf);
uint8_t* const message = i2c_message->buf;
const size_t message_length = i2c_message->len;
CHECK(message);
CHECK_GT(message_length, 0u);
if (i2c_message->addr != ExternalDisplay::kDdcI2CAddress) {
LOG(ERROR) << "Ignoring operation with I2C address " << i2c_message->addr;
return false;
}
// Write request.
if (i2c_message->flags == 0) {
if (report_write_failure_)
return false;
sent_messages_.push_back(base::HexEncode(message, message_length));
return true;
}
// Read request.
if (i2c_message->flags == I2C_M_RD) {
if (report_read_failure_)
return false;
if (message_length != reply_message_.size()) {
LOG(ERROR) << "Got request to read " << message_length << " byte(s); "
<< "expected " << reply_message_.size();
reply_message_.clear();
return false;
}
memcpy(message, &(reply_message_[0]), message_length);
reply_message_.clear();
return true;
}
LOG(ERROR) << "Ignoring operation with I2C flags " << i2c_message->flags;
return false;
}
private:
// Sent messages, converted to hexadecimal strings, in the order they were
// transmitted.
std::vector<std::string> sent_messages_;
// Message that should be returned in response to read requests.
// The message will be cleared after the next read request.
std::vector<uint8_t> reply_message_;
// True if either writes or reads should report failure.
bool report_write_failure_;
bool report_read_failure_;
};
} // namespace
class ExternalDisplayTest : public testing::Test {
public:
ExternalDisplayTest()
: delegate_(new TestDelegate),
display_(std::unique_ptr<ExternalDisplay::Delegate>(delegate_)),
test_api_(&display_) {
request_brightness_message_ =
// Message header.
Hex(ExternalDisplay::kDdcHostAddress) +
Hex(ExternalDisplay::kDdcMessageBodyLengthMask | 2) +
// Message body.
Hex(ExternalDisplay::kDdcGetCommand) +
Hex(ExternalDisplay::kDdcBrightnessIndex) +
// Checksum byte.
Hex(ExternalDisplay::kDdcDisplayAddress ^
ExternalDisplay::kDdcHostAddress ^
(ExternalDisplay::kDdcMessageBodyLengthMask | 2) ^
ExternalDisplay::kDdcGetCommand ^
ExternalDisplay::kDdcBrightnessIndex);
}
~ExternalDisplayTest() override {}
protected:
// Updates the checksum byte that's already present at the end of |message|.
void UpdateChecksum(uint8_t starting_value, std::vector<uint8_t>* message) {
uint8_t checksum = starting_value;
for (size_t i = 0; i < message->size() - 1; ++i)
checksum ^= (*message)[i];
(*message)[message->size() - 1] = checksum;
}
// Generate a reply to a request to get the brightness, suitable for passing
// to TestDelegate::set_reply_message().
std::vector<uint8_t> GetBrightnessReply(uint16_t current_brightness,
uint16_t max_brightness) {
std::vector<uint8_t> message;
// Message header.
message.push_back(ExternalDisplay::kDdcDisplayAddress);
message.push_back(ExternalDisplay::kDdcMessageBodyLengthMask | 8);
// Message body.
message.push_back(ExternalDisplay::kDdcGetReplyCommand);
message.push_back(0x0); // Result code.
message.push_back(ExternalDisplay::kDdcBrightnessIndex);
message.push_back(0x0); // VCP type code.
message.push_back(max_brightness >> 8);
message.push_back(max_brightness & 0xff);
message.push_back(current_brightness >> 8);
message.push_back(current_brightness & 0xff);
// Checksum byte.
message.push_back(0x0);
UpdateChecksum(ExternalDisplay::kDdcVirtualHostAddress, &message);
return message;
}
// Returns the string representation of the message that should be sent to set
// the brightness to |brightness|.
std::string GetSetBrightnessMessage(uint16_t brightness) {
const uint8_t high_byte = brightness >> 8;
const uint8_t low_byte = brightness & 0xff;
return
// Message header.
Hex(ExternalDisplay::kDdcHostAddress) +
Hex(ExternalDisplay::kDdcMessageBodyLengthMask | 4) +
// Message body.
Hex(ExternalDisplay::kDdcSetCommand) +
Hex(ExternalDisplay::kDdcBrightnessIndex) + Hex(high_byte) +
Hex(low_byte) +
// Checksum byte.
Hex(ExternalDisplay::kDdcDisplayAddress ^
ExternalDisplay::kDdcHostAddress ^
(ExternalDisplay::kDdcMessageBodyLengthMask | 4) ^
ExternalDisplay::kDdcSetCommand ^
ExternalDisplay::kDdcBrightnessIndex ^ high_byte ^ low_byte);
}
// Pops and returns a string representation of the metric stored in
// |metrics_sender_|. Crashes if multiple metrics are stored.
std::string PopMetric() {
CHECK_LE(metrics_sender_.num_metrics(), 1);
const std::string metric = metrics_sender_.GetMetric(0);
metrics_sender_.clear_metrics();
return metric;
}
// What a message requesting the display brightness should look like.
std::string request_brightness_message_;
MetricsSenderStub metrics_sender_;
TestDelegate* delegate_; // weak pointer
ExternalDisplay display_;
ExternalDisplay::TestApi test_api_;
};
TEST_F(ExternalDisplayTest, BasicCommunication) {
// Asking for brightness to be increased by 10% should result in a "get
// brightness" request being sent.
display_.AdjustBrightnessByPercent(10.0);
EXPECT_EQ(request_brightness_message_, delegate_->PopSentMessage());
EXPECT_EQ(MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessRequestResultName,
static_cast<int>(ExternalDisplay::SendResult::SUCCESS),
metrics::kExternalDisplayResultMax)
.ToString(),
PopMetric());
// After the timer fires, the reply should be read and a request to set the
// brightness to 60 should be sent.
delegate_->set_reply_message(GetBrightnessReply(50, 100));
EXPECT_EQ(ExternalDisplay::kDdcGetDelayMs,
test_api_.GetTimerDelay().InMilliseconds());
ASSERT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ(GetSetBrightnessMessage(60), delegate_->PopSentMessage());
// The successful read and write should both be reported.
EXPECT_EQ(2, metrics_sender_.num_metrics());
EXPECT_EQ(MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessReadResultName,
static_cast<int>(ExternalDisplay::ReceiveResult::SUCCESS),
metrics::kExternalDisplayResultMax)
.ToString(),
metrics_sender_.GetMetric(0));
EXPECT_EQ(MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessWriteResultName,
static_cast<int>(ExternalDisplay::SendResult::SUCCESS),
metrics::kExternalDisplayResultMax)
.ToString(),
metrics_sender_.GetMetric(1));
metrics_sender_.clear_metrics();
// Asking for more changes shouldn't result in requests being sent at first,
// since no time has passed since the previous request. After the timer fires,
// a new request should be sent containing both adjustments.
display_.AdjustBrightnessByPercent(20.0);
EXPECT_EQ("", delegate_->PopSentMessage());
display_.AdjustBrightnessByPercent(5.0);
EXPECT_EQ("", delegate_->PopSentMessage());
EXPECT_EQ(ExternalDisplay::kDdcSetDelayMs,
test_api_.GetTimerDelay().InMilliseconds());
ASSERT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ(GetSetBrightnessMessage(85), delegate_->PopSentMessage());
// The timer should fire again when it's safe to send another message, but
// nothing should happen since there are no pending adjustments.
EXPECT_EQ(ExternalDisplay::kDdcSetDelayMs,
test_api_.GetTimerDelay().InMilliseconds());
EXPECT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ("", delegate_->PopSentMessage());
EXPECT_FALSE(test_api_.TriggerTimeout());
// Let enough time pass for the cached brightness to be invalidated.
// Asking for another adjustment should result in the brightness being
// re-read.
test_api_.AdvanceTime(base::TimeDelta::FromMilliseconds(
ExternalDisplay::kCachedBrightnessValidMs + 10));
display_.AdjustBrightnessByPercent(-10.0);
EXPECT_EQ(request_brightness_message_, delegate_->PopSentMessage());
// Pretend like the user decreased the brightness via physical buttons on the
// monitor and reply that the current level is 30.
delegate_->set_reply_message(GetBrightnessReply(30, 100));
EXPECT_EQ(ExternalDisplay::kDdcGetDelayMs,
test_api_.GetTimerDelay().InMilliseconds());
ASSERT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ(GetSetBrightnessMessage(20), delegate_->PopSentMessage());
}
TEST_F(ExternalDisplayTest, InvalidBrightnessReplies) {
struct TestCase {
// Reply message that should be sent from the display.
std::vector<uint8_t> reply;
// Metric enum value that should be reported after the failed read.
ExternalDisplay::ReceiveResult metric;
// Description of what's being tested.
std::string description;
};
std::vector<TestCase> test_cases;
std::vector<uint8_t> reply = GetBrightnessReply(50, 100);
reply[reply.size() - 1] += 1;
test_cases.push_back(TestCase{reply,
ExternalDisplay::ReceiveResult::BAD_CHECKSUM,
"incorrect checksum"});
reply = GetBrightnessReply(50, 100);
reply[0] += 1;
UpdateChecksum(ExternalDisplay::kDdcVirtualHostAddress, &reply);
test_cases.push_back(TestCase{reply,
ExternalDisplay::ReceiveResult::BAD_ADDRESS,
"incorrect source address"});
reply = GetBrightnessReply(50, 100);
reply[1] = ExternalDisplay::kDdcMessageBodyLengthMask | 9; // Should be 8.
UpdateChecksum(ExternalDisplay::kDdcVirtualHostAddress, &reply);
test_cases.push_back(TestCase{reply,
ExternalDisplay::ReceiveResult::BAD_LENGTH,
"incorrect body length"});
reply = GetBrightnessReply(50, 100);
reply[2] = ExternalDisplay::kDdcSetCommand;
UpdateChecksum(ExternalDisplay::kDdcVirtualHostAddress, &reply);
test_cases.push_back(TestCase{
reply, ExternalDisplay::ReceiveResult::BAD_COMMAND, "non-reply command"});
reply = GetBrightnessReply(50, 100);
reply[3] = 0x1; // Should be 0x0 for success.
UpdateChecksum(ExternalDisplay::kDdcVirtualHostAddress, &reply);
test_cases.push_back(TestCase{reply,
ExternalDisplay::ReceiveResult::BAD_RESULT,
"non-zero result code"});
reply = GetBrightnessReply(50, 100);
reply[4] = ExternalDisplay::kDdcBrightnessIndex + 1;
UpdateChecksum(ExternalDisplay::kDdcVirtualHostAddress, &reply);
test_cases.push_back(TestCase{reply,
ExternalDisplay::ReceiveResult::BAD_INDEX,
"non-brightness index"});
// Run through each test case, making sure that no subsequent request is sent
// after the bogus reply is returned. The timer also shouldn't be rescheduled.
for (size_t i = 0; i < test_cases.size(); ++i) {
SCOPED_TRACE(test_cases[i].description);
display_.AdjustBrightnessByPercent(10.0);
ASSERT_EQ(request_brightness_message_, delegate_->PopSentMessage());
EXPECT_EQ(MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessRequestResultName,
static_cast<int>(ExternalDisplay::SendResult::SUCCESS),
metrics::kExternalDisplayResultMax)
.ToString(),
PopMetric());
delegate_->set_reply_message(test_cases[i].reply);
ASSERT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ(MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessReadResultName,
static_cast<int>(test_cases[i].metric),
metrics::kExternalDisplayResultMax)
.ToString(),
PopMetric());
EXPECT_EQ("", delegate_->PopSentMessage());
EXPECT_FALSE(test_api_.TriggerTimeout());
}
}
TEST_F(ExternalDisplayTest, CommunicationFailures) {
// If the initial brightness request fails, the timer should be stopped.
delegate_->set_report_write_failure(true);
display_.AdjustBrightnessByPercent(10.0);
EXPECT_FALSE(test_api_.TriggerTimeout());
EXPECT_EQ(MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessRequestResultName,
static_cast<int>(ExternalDisplay::SendResult::IOCTL_FAILED),
metrics::kExternalDisplayResultMax)
.ToString(),
PopMetric());
// Now let the initial request succeed but make the read fail. The timer
// should be stopped.
delegate_->set_report_write_failure(false);
delegate_->set_report_read_failure(true);
display_.AdjustBrightnessByPercent(10.0);
EXPECT_EQ(request_brightness_message_, delegate_->PopSentMessage());
ASSERT_TRUE(test_api_.TriggerTimeout());
EXPECT_FALSE(test_api_.TriggerTimeout());
EXPECT_EQ(2, metrics_sender_.num_metrics());
EXPECT_EQ(MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessRequestResultName,
static_cast<int>(ExternalDisplay::SendResult::SUCCESS),
metrics::kExternalDisplayResultMax)
.ToString(),
metrics_sender_.GetMetric(0));
EXPECT_EQ(MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessReadResultName,
static_cast<int>(ExternalDisplay::ReceiveResult::IOCTL_FAILED),
metrics::kExternalDisplayResultMax)
.ToString(),
metrics_sender_.GetMetric(1));
metrics_sender_.clear_metrics();
// Let the initial request and read succeed, but make the attempt to change
// the brightness fail. The timer should be stopped.
delegate_->set_report_read_failure(false);
display_.AdjustBrightnessByPercent(10.0);
EXPECT_EQ(request_brightness_message_, delegate_->PopSentMessage());
delegate_->set_report_write_failure(true);
delegate_->set_reply_message(GetBrightnessReply(50, 100));
ASSERT_TRUE(test_api_.TriggerTimeout());
EXPECT_FALSE(test_api_.TriggerTimeout());
EXPECT_EQ(3, metrics_sender_.num_metrics());
EXPECT_EQ(MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessRequestResultName,
static_cast<int>(ExternalDisplay::SendResult::SUCCESS),
metrics::kExternalDisplayResultMax)
.ToString(),
metrics_sender_.GetMetric(0));
EXPECT_EQ(MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessReadResultName,
static_cast<int>(ExternalDisplay::ReceiveResult::SUCCESS),
metrics::kExternalDisplayResultMax)
.ToString(),
metrics_sender_.GetMetric(1));
EXPECT_EQ(MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessWriteResultName,
static_cast<int>(ExternalDisplay::SendResult::IOCTL_FAILED),
metrics::kExternalDisplayResultMax)
.ToString(),
metrics_sender_.GetMetric(2));
metrics_sender_.clear_metrics();
// The previously-read brightness should still be cached, so another
// adjustment attempt should be attempted immediately. Let it succeed this
// time.
delegate_->set_report_write_failure(false);
display_.AdjustBrightnessByPercent(10.0);
EXPECT_EQ(GetSetBrightnessMessage(60), delegate_->PopSentMessage());
EXPECT_EQ(MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessWriteResultName,
static_cast<int>(ExternalDisplay::SendResult::SUCCESS),
metrics::kExternalDisplayResultMax)
.ToString(),
PopMetric());
}
TEST_F(ExternalDisplayTest, MinimumAndMaximumBrightness) {
// A request to go below 0 should be capped.
display_.AdjustBrightnessByPercent(-80.0);
EXPECT_EQ(request_brightness_message_, delegate_->PopSentMessage());
delegate_->set_reply_message(GetBrightnessReply(50, 80));
ASSERT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ(GetSetBrightnessMessage(0), delegate_->PopSentMessage());
// Now that the brightness is at 0, a request to decrease it further shouldn't
// result in a message being sent to the display.
display_.AdjustBrightnessByPercent(-10.0);
EXPECT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ("", delegate_->PopSentMessage());
// Requests above the maximum brightness should also be capped.
display_.AdjustBrightnessByPercent(120.0);
EXPECT_EQ(GetSetBrightnessMessage(80), delegate_->PopSentMessage());
// Trying to increase the brightness further shouldn't do anything.
display_.AdjustBrightnessByPercent(10.0);
EXPECT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ("", delegate_->PopSentMessage());
// Decrease the brightness a bit, and then check that if two adjustments that
// cancel each other are requested before it's safe to send another request,
// they cancel each other out and result in nothing being sent to the display.
display_.AdjustBrightnessByPercent(-10.0);
EXPECT_EQ(GetSetBrightnessMessage(72), delegate_->PopSentMessage());
display_.AdjustBrightnessByPercent(-5.0);
display_.AdjustBrightnessByPercent(5.0);
EXPECT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ("", delegate_->PopSentMessage());
}
TEST_F(ExternalDisplayTest, Rounding) {
// 5.3% should be sent to the display as 5/100.
display_.AdjustBrightnessByPercent(5.3);
EXPECT_EQ(request_brightness_message_, delegate_->PopSentMessage());
delegate_->set_reply_message(GetBrightnessReply(0, 100));
ASSERT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ(GetSetBrightnessMessage(5), delegate_->PopSentMessage());
// A 4.4% increase goes to 9.7%, which is rounded to 10/100.
display_.AdjustBrightnessByPercent(4.4);
ASSERT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ(GetSetBrightnessMessage(10), delegate_->PopSentMessage());
// A 0.3% decrease goes to 9.4%, which is rounded to 9/100.
display_.AdjustBrightnessByPercent(-0.3);
ASSERT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ(GetSetBrightnessMessage(9), delegate_->PopSentMessage());
// A 0.6% decrease goes to 8.8%, which is still rounded to 9 and shouldn't
// trigger an update.
display_.AdjustBrightnessByPercent(-0.6);
ASSERT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ("", delegate_->PopSentMessage());
}
TEST_F(ExternalDisplayTest, ZeroMax) {
// Make the display return a maximum value of zero.
display_.AdjustBrightnessByPercent(5.0);
EXPECT_EQ(request_brightness_message_, delegate_->PopSentMessage());
EXPECT_EQ(MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessRequestResultName,
static_cast<int>(ExternalDisplay::SendResult::SUCCESS),
metrics::kExternalDisplayResultMax)
.ToString(),
PopMetric());
// ExternalDisplay should report a failure and avoid writing an updated level.
delegate_->set_reply_message(GetBrightnessReply(0, 0));
ASSERT_TRUE(test_api_.TriggerTimeout());
EXPECT_EQ(
MetricsSenderStub::Metric::CreateEnum(
metrics::kExternalBrightnessReadResultName,
static_cast<int>(ExternalDisplay::ReceiveResult::ZERO_MAX_VALUE),
metrics::kExternalDisplayResultMax)
.ToString(),
PopMetric());
EXPECT_FALSE(test_api_.TriggerTimeout());
}
} // namespace system
} // namespace power_manager