blob: 83626cc63c51a3027f8d97c97be0e95b604408c3 [file] [log] [blame]
// Copyright 2020 The Fuchsia 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 <lib/async-loop/cpp/loop.h>
#include <lib/async_patterns/testing/cpp/dispatcher_bound.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/device-protocol/i2c-channel.h>
#include <lib/fake-i2c/fake-i2c.h>
#include <zircon/errors.h>
#include <algorithm>
#include <numeric>
#include <vector>
#include <zxtest/zxtest.h>
#include "src/devices/testing/mock-ddk/mock-device.h"
namespace {
// An I2C device that requires retries.
class FlakyI2cDevice : public fake_i2c::FakeI2c {
protected:
zx_status_t Transact(const uint8_t* write_buffer, size_t write_buffer_size, uint8_t* read_buffer,
size_t* read_buffer_size) override {
count_++;
// Unique errors below to check for retries.
switch (count_) {
// clang-format off
case 1: return ZX_ERR_INTERNAL; break;
case 2: return ZX_ERR_NOT_SUPPORTED; break;
case 3: return ZX_ERR_NO_RESOURCES; break;
case 4: return ZX_ERR_NO_MEMORY; break;
case 5: *read_buffer_size = 1; return ZX_OK; break;
default: ZX_ASSERT(0); // Anything else is an error.
// clang-format on
}
return ZX_OK;
}
private:
size_t count_ = 0;
};
class I2cDevice : public fidl::WireServer<fuchsia_hardware_i2c::Device> {
public:
void Transfer(TransferRequestView request, TransferCompleter::Sync& completer) override {
const fidl::VectorView<fuchsia_hardware_i2c::wire::Transaction> transactions =
request->transactions;
if (std::any_of(transactions.cbegin(), transactions.cend(),
[](const fuchsia_hardware_i2c::wire::Transaction& t) {
return !t.has_data_transfer();
})) {
completer.ReplyError(ZX_ERR_INVALID_ARGS);
return;
}
fidl::Arena arena;
fidl::ObjectView<fuchsia_hardware_i2c::wire::DeviceTransferResponse> response(arena);
stop_.reserve(transactions.count());
const size_t write_size = std::accumulate(
transactions.cbegin(), transactions.cend(), 0,
[](size_t a, const fuchsia_hardware_i2c::wire::Transaction& b) {
return a +
(b.data_transfer().is_write_data() ? b.data_transfer().write_data().count() : 0);
});
tx_data_.resize(write_size);
const size_t read_count = std::count_if(transactions.cbegin(), transactions.cend(),
[](const fuchsia_hardware_i2c::wire::Transaction& t) {
return t.data_transfer().is_read_size();
});
response->read_data = {arena, read_count};
size_t tx_offset = 0;
size_t rx_transaction = 0;
size_t rx_offset = 0;
auto stop_it = stop_.begin();
for (const auto& transaction : transactions) {
if (transaction.data_transfer().is_read_size()) {
// If this is a write/read, pass back all of the expected read data, regardless of how much
// the client requested. This allows the truncation behavior to be tested.
const size_t read_size =
read_count == 1 ? rx_data_.size() : transaction.data_transfer().read_size();
// Copy the expected RX data to each read transaction.
auto& read_data = response->read_data[rx_transaction++];
read_data = {arena, read_size};
if (rx_offset + read_data.count() > rx_data_.size()) {
completer.ReplyError(ZX_ERR_OUT_OF_RANGE);
return;
}
memcpy(read_data.data(), &rx_data_[rx_offset], read_data.count());
rx_offset += transaction.data_transfer().read_size();
} else {
// Serialize and store the write transaction.
const auto& write_data = transaction.data_transfer().write_data();
memcpy(&tx_data_[tx_offset], write_data.data(), write_data.count());
tx_offset += write_data.count();
}
*stop_it++ = transaction.has_stop() ? transaction.stop() : false;
}
completer.Reply(::fit::ok(response.get()));
}
void GetName(GetNameCompleter::Sync& completer) override {
completer.ReplyError(ZX_ERR_NOT_SUPPORTED);
}
const std::vector<uint8_t>& tx_data() const { return tx_data_; }
void set_rx_data(std::vector<uint8_t> rx_data) { rx_data_ = std::move(rx_data); }
const std::vector<bool>& stop() const { return stop_; }
private:
std::vector<uint8_t> tx_data_;
std::vector<uint8_t> rx_data_;
std::vector<bool> stop_;
};
template <typename Device>
class I2cServer {
public:
explicit I2cServer(async_dispatcher_t* dispatcher)
: dispatcher_(dispatcher), outgoing_(dispatcher) {}
void BindProtocol(fidl::ServerEnd<fuchsia_hardware_i2c::Device> server_end) {
bindings_.AddBinding(dispatcher_, std::move(server_end), &i2c_dev_,
fidl::kIgnoreBindingClosure);
}
void PublishService(fidl::ServerEnd<fuchsia_io::Directory> directory) {
zx::result service_result = outgoing_.AddService<fuchsia_hardware_i2c::Service>(
fuchsia_hardware_i2c::Service::InstanceHandler({
.device = bindings_.CreateHandler(&i2c_dev_, dispatcher_, fidl::kIgnoreBindingClosure),
}));
ASSERT_OK(service_result.status_value());
ASSERT_OK(outgoing_.Serve(std::move(directory)));
}
Device& i2c_dev() { return i2c_dev_; }
private:
async_dispatcher_t* dispatcher_;
component::OutgoingDirectory outgoing_;
fidl::ServerBindingGroup<fuchsia_hardware_i2c::Device> bindings_;
Device i2c_dev_;
};
class I2cFlakyChannelTest : public zxtest::Test {
public:
using Server = I2cServer<FlakyI2cDevice>;
I2cFlakyChannelTest() : loop_(&kAsyncLoopConfigNeverAttachToThread) { loop_.StartThread(); }
void SetUp() final {
zx::result endpoints = fidl::CreateEndpoints<fuchsia_hardware_i2c::Device>();
ASSERT_OK(endpoints.status_value());
i2c_server_.AsyncCall(&Server::BindProtocol, std::move(endpoints->server));
i2c_channel_.emplace(std::move(endpoints->client));
}
ddk::I2cChannel& i2c_channel() { return i2c_channel_.value(); }
async_patterns::TestDispatcherBound<Server>& i2c_server() { return i2c_server_; }
private:
async::Loop loop_;
async_patterns::TestDispatcherBound<Server> i2c_server_{loop_.dispatcher(), std::in_place,
async_patterns::PassDispatcher};
std::optional<ddk::I2cChannel> i2c_channel_;
};
TEST_F(I2cFlakyChannelTest, NoRetries) {
// No retry, the first error is returned.
uint8_t buffer[1] = {0x12};
constexpr uint8_t kNumberOfRetries = 0;
auto ret = i2c_channel().WriteSyncRetries(buffer, sizeof(buffer), kNumberOfRetries, zx::usec(1));
EXPECT_EQ(ret.status, ZX_ERR_INTERNAL);
EXPECT_EQ(ret.retries, 0);
}
TEST_F(I2cFlakyChannelTest, RetriesAllFail) {
// 2 retries, corresponding error is returned. The first time Transact is called we get a
// ZX_ERR_INTERNAL. Then the first retry gives us ZX_ERR_NOT_SUPPORTED and then the second
// gives us ZX_ERR_NO_RESOURCES.
constexpr uint8_t kNumberOfRetries = 2;
uint8_t buffer[1] = {0x34};
auto ret =
i2c_channel().ReadSyncRetries(0x56, buffer, sizeof(buffer), kNumberOfRetries, zx::usec(1));
EXPECT_EQ(ret.status, ZX_ERR_NO_RESOURCES);
EXPECT_EQ(ret.retries, 2);
}
TEST_F(I2cFlakyChannelTest, RetriesOk) {
// 4 retries requested but no error, return ok.
uint8_t tx_buffer[1] = {0x78};
uint8_t rx_buffer[1] = {0x90};
constexpr uint8_t kNumberOfRetries = 5;
auto ret = i2c_channel().WriteReadSyncRetries(tx_buffer, sizeof(tx_buffer), rx_buffer,
sizeof(rx_buffer), kNumberOfRetries, zx::usec(1));
EXPECT_EQ(ret.status, ZX_OK);
EXPECT_EQ(ret.retries, 4);
}
class I2cChannelTest : public zxtest::Test {
public:
using Server = I2cServer<I2cDevice>;
I2cChannelTest() : loop_(&kAsyncLoopConfigNeverAttachToThread) { loop_.StartThread(); }
void SetUp() final {
zx::result endpoints = fidl::CreateEndpoints<fuchsia_hardware_i2c::Device>();
ASSERT_OK(endpoints.status_value());
i2c_server_.AsyncCall(&Server::BindProtocol, std::move(endpoints->server));
i2c_channel_.emplace(std::move(endpoints->client));
}
ddk::I2cChannel& i2c_channel() { return i2c_channel_.value(); }
async_patterns::TestDispatcherBound<Server>& i2c_server() { return i2c_server_; }
private:
async::Loop loop_;
async_patterns::TestDispatcherBound<Server> i2c_server_{loop_.dispatcher(), std::in_place,
async_patterns::PassDispatcher};
std::optional<ddk::I2cChannel> i2c_channel_;
};
TEST_F(I2cChannelTest, FidlRead) {
static constexpr std::array<uint8_t, 4> expected_rx_data{0x12, 0x34, 0xab, 0xcd};
i2c_server().SyncCall([](Server* server) {
server->i2c_dev().set_rx_data(
{expected_rx_data.data(), expected_rx_data.data() + expected_rx_data.size()});
});
uint8_t buf[4];
EXPECT_OK(i2c_channel().ReadSync(0x89, buf, expected_rx_data.size()));
i2c_server().SyncCall([](Server* server) {
ASSERT_EQ(server->i2c_dev().tx_data().size(), 1);
EXPECT_EQ(server->i2c_dev().tx_data()[0], 0x89);
});
EXPECT_BYTES_EQ(buf, expected_rx_data.data(), expected_rx_data.size());
uint8_t buf1[5];
// I2cChannel will copy as much data as it receives, even if it is less than the amount requested.
EXPECT_OK(i2c_channel().ReadSync(0x98, buf1, sizeof(buf1)));
i2c_server().SyncCall([](Server* server) {
ASSERT_EQ(server->i2c_dev().tx_data().size(), 1);
EXPECT_EQ(server->i2c_dev().tx_data()[0], 0x98);
});
EXPECT_BYTES_EQ(buf1, expected_rx_data.data(), expected_rx_data.size());
uint8_t buf2[3];
// I2cChannel will copy no more than the amount requested.
EXPECT_OK(i2c_channel().ReadSync(0x18, buf2, sizeof(buf2)));
i2c_server().SyncCall([](Server* server) {
ASSERT_EQ(server->i2c_dev().tx_data().size(), 1);
EXPECT_EQ(server->i2c_dev().tx_data()[0], 0x18);
});
EXPECT_BYTES_EQ(buf2, expected_rx_data.data(), sizeof(buf2));
}
TEST_F(I2cChannelTest, FidlWrite) {
static constexpr std::array<uint8_t, 4> expected_tx_data{0x0f, 0x1e, 0x2d, 0x3c};
EXPECT_OK(i2c_channel().WriteSync(expected_tx_data.data(), expected_tx_data.size()));
i2c_server().SyncCall([](Server* server) {
ASSERT_EQ(server->i2c_dev().tx_data().size(), expected_tx_data.size());
EXPECT_BYTES_EQ(server->i2c_dev().tx_data().data(), expected_tx_data.data(),
expected_tx_data.size());
});
}
TEST_F(I2cChannelTest, FidlWriteRead) {
static constexpr std::array<uint8_t, 4> expected_rx_data{0x12, 0x34, 0xab, 0xcd};
static constexpr std::array<uint8_t, 4> expected_tx_data{0x0f, 0x1e, 0x2d, 0x3c};
i2c_server().SyncCall([](Server* server) {
server->i2c_dev().set_rx_data(
{expected_rx_data.data(), expected_rx_data.data() + expected_rx_data.size()});
});
uint8_t buf[4];
EXPECT_OK(i2c_channel().WriteReadSync(expected_tx_data.data(), expected_tx_data.size(), buf,
expected_rx_data.size()));
i2c_server().SyncCall([](Server* server) {
ASSERT_EQ(server->i2c_dev().tx_data().size(), expected_tx_data.size());
EXPECT_BYTES_EQ(server->i2c_dev().tx_data().data(), expected_tx_data.data(),
expected_tx_data.size());
});
EXPECT_BYTES_EQ(buf, expected_rx_data.data(), expected_rx_data.size());
uint8_t buf1[5];
EXPECT_OK(i2c_channel().WriteReadSync(expected_tx_data.data(), expected_tx_data.size(), buf1,
sizeof(buf1)));
i2c_server().SyncCall([](Server* server) {
EXPECT_BYTES_EQ(server->i2c_dev().tx_data().data(), expected_tx_data.data(),
expected_tx_data.size());
});
EXPECT_BYTES_EQ(buf1, expected_rx_data.data(), expected_rx_data.size());
uint8_t buf2[3];
EXPECT_OK(i2c_channel().WriteReadSync(expected_tx_data.data(), expected_tx_data.size(), buf2,
sizeof(buf2)));
i2c_server().SyncCall([](Server* server) {
EXPECT_BYTES_EQ(server->i2c_dev().tx_data().data(), expected_tx_data.data(),
expected_tx_data.size());
});
EXPECT_BYTES_EQ(buf2, expected_rx_data.data(), sizeof(buf2));
EXPECT_OK(i2c_channel().WriteReadSync(nullptr, 0, buf, expected_rx_data.size()));
i2c_server().SyncCall([](Server* server) { EXPECT_EQ(server->i2c_dev().tx_data().size(), 0); });
EXPECT_BYTES_EQ(buf, expected_rx_data.data(), expected_rx_data.size());
}
class I2cChannelServiceTest : public zxtest::Test {
public:
using Server = I2cServer<I2cDevice>;
I2cChannelServiceTest() : loop_(&kAsyncLoopConfigNeverAttachToThread) { loop_.StartThread(); }
void SetUp() final {
zx::result endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>();
ASSERT_OK(endpoints.status_value());
i2c_server_.AsyncCall(&Server::PublishService, std::move(endpoints->server));
directory_ = std::move(endpoints->client);
}
async_patterns::TestDispatcherBound<Server>& i2c_server() { return i2c_server_; }
fidl::ClientEnd<fuchsia_io::Directory> TakeDirectory() { return std::move(directory_); }
private:
async::Loop loop_;
async_patterns::TestDispatcherBound<Server> i2c_server_{loop_.dispatcher(), std::in_place,
async_patterns::PassDispatcher};
fidl::ClientEnd<fuchsia_io::Directory> directory_;
};
TEST_F(I2cChannelServiceTest, GetFidlProtocolFromParent) {
auto parent = MockDevice::FakeRootParent();
parent->AddFidlService(fuchsia_hardware_i2c::Service::Name, TakeDirectory());
ddk::I2cChannel i2c_channel{parent.get()};
ASSERT_TRUE(i2c_channel.is_valid());
// Issue a simple call to make sure the connection is working.
i2c_server().SyncCall([](Server* server) { server->i2c_dev().set_rx_data({0xab}); });
uint8_t rx;
EXPECT_OK(i2c_channel.ReadSync(0x89, &rx, 1));
i2c_server().SyncCall([](Server* server) {
ASSERT_EQ(server->i2c_dev().tx_data().size(), 1);
EXPECT_EQ(server->i2c_dev().tx_data()[0], 0x89);
});
EXPECT_EQ(rx, 0xab);
// Move the client and verify that the new one is functional.
ddk::I2cChannel new_client = std::move(i2c_channel);
i2c_server().SyncCall([](Server* server) { server->i2c_dev().set_rx_data({0x12}); });
EXPECT_OK(new_client.ReadSync(0x34, &rx, 1));
i2c_server().SyncCall([](Server* server) {
ASSERT_EQ(server->i2c_dev().tx_data().size(), 1);
EXPECT_EQ(server->i2c_dev().tx_data()[0], 0x34);
});
EXPECT_EQ(rx, 0x12);
}
TEST_F(I2cChannelServiceTest, GetFidlProtocolFromFragment) {
auto parent = MockDevice::FakeRootParent();
parent->AddFidlService(fuchsia_hardware_i2c::Service::Name, TakeDirectory(), "fragment-name");
ddk::I2cChannel i2c_channel{parent.get(), "fragment-name"};
ASSERT_TRUE(i2c_channel.is_valid());
i2c_server().SyncCall([](Server* server) { server->i2c_dev().set_rx_data({0x56}); });
uint8_t rx;
EXPECT_OK(i2c_channel.ReadSync(0x78, &rx, 1));
i2c_server().SyncCall([](Server* server) {
ASSERT_EQ(server->i2c_dev().tx_data().size(), 1);
EXPECT_EQ(server->i2c_dev().tx_data()[0], 0x78);
});
EXPECT_EQ(rx, 0x56);
}
} // namespace