pw_i2c: Adds a mocked initiator implementation This is useful as a reusable mock when writing drivers. Specifically this allows a driver developer to specify a set of mocked i2c transactions. This does not require physical hardware and is ideal for unit testing a driver. Change-Id: I4e0f13450f84feeefeef534b6c46192462c18a8b Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/37920 Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com> Commit-Queue: Ewout van Bekkum <ewout@google.com> Reviewed-by: Ewout van Bekkum <ewout@google.com> Pigweed-Auto-Submit: Ewout van Bekkum <ewout@google.com>
diff --git a/pw_i2c/BUILD b/pw_i2c/BUILD index 7ce2297..820c9fe 100644 --- a/pw_i2c/BUILD +++ b/pw_i2c/BUILD
@@ -89,6 +89,33 @@ ], deps = [ ":address", + "//pw_assert", + "//pw_unit_test", + ], +) + +pw_cc_library( + name = "initiator_mock", + testonly = True, + srcs = ["initiator_mock.cc"], + hdrs = ["public/pw_i2c/initiator_mock.h"], + includes = ["public"], + deps = [ + ":address", + ":initiator", + "//pw_assert", + "//pw_unit_test", + ], +) + +pw_cc_test( + name = "initiator_mock_test", + srcs = [ + "initiator_mock_test.cc", + ], + deps = [ + ":initiator_mock", + "//pw_bytes", "//pw_unit_test", ], )
diff --git a/pw_i2c/BUILD.gn b/pw_i2c/BUILD.gn index 81fa6c8..99ba0c3 100644 --- a/pw_i2c/BUILD.gn +++ b/pw_i2c/BUILD.gn
@@ -69,6 +69,17 @@ deps = [ "$dir_pw_assert" ] } +pw_source_set("mock") { + public_configs = [ ":public_include_path" ] + public = [ "public/pw_i2c/initiator_mock.h" ] + sources = [ "initiator_mock.cc" ] + public_deps = [ + ":initiator", + "$dir_pw_assert", + ] +} + +# TODO: add mock_test here once chrono backend is supported for stm32f429i-disc1 pw_test_group("tests") { tests = [ ":address_test", @@ -97,6 +108,11 @@ ] } +pw_test("initiator_mock_test") { + sources = [ "initiator_mock_test.cc" ] + deps = [ ":initiator_mock" ] +} + pw_doc_group("docs") { sources = [ "docs.rst" ] }
diff --git a/pw_i2c/docs.rst b/pw_i2c/docs.rst index 976820e..9129edc 100644 --- a/pw_i2c/docs.rst +++ b/pw_i2c/docs.rst
@@ -33,3 +33,45 @@ understanding of the capabilities of their device such as register address sizes, register data sizes, byte addressability, bulk transactions, etc in order to effectively use this interface. + +pw::i2c::MockInitiator +---------------------- + +A generic mocked backend for for pw::i2c::Initiator. This is specifically +intended for use when developing drivers for i2c devices. This is structured +around a set of 'transactions' where each transaction contains a write, read and +a timeout. A transaction list can then be passed to the MockInitiator, where +each consecutive call to read/write will iterate to the next transaction in the +list. An example of this is shown below: + +.. code-block:: cpp + + using pw::i2c::Address; + using pw::i2c::MakeExpectedTransactionlist; + using pw::i2c::MockInitiator; + using pw::i2c::WriteTransaction; + using std::literals::chrono_literals::ms; + + constexpr Address kAddress1 = Address::SevenBit<0x01>(); + constexpr auto kExpectWrite1 = pw::bytes::Array<1, 2, 3, 4, 5>(); + constexpr auto kExpectWrite2 = pw::bytes::Array<3, 4, 5>(); + auto expected_transactions = MakeExpectedTransactionArray( + {WriteTransaction(pw::OkStatus(), kAddress1, kExpectWrite1, 1ms), + WriteTransaction(pw::OkStatus(), kAddress2, kExpectWrite2, 1ms)}); + MockInitiator i2c_mock(expected_transactions); + + // Begin driver code + ConstByteSpan write1 = kExpectWrite1; + // write1 is ok as i2c_mock expects {1, 2, 3, 4, 5} == {1, 2, 3, 4, 5} + Status status = i2c_mock.WriteFor(kAddress1, write1, 2ms); + + // Takes the first two bytes from the expected array to build a mismatching + // span to write. + ConstByteSpan write2 = std::span(kExpectWrite2).first(2); + // write2 fails as i2c_mock expects {3, 4, 5} != {3, 4} + status = i2c_mock.WriteFor(kAddress2, write2, 2ms); + // End driver code + + // Optionally check if the mocked transaction list has been exhausted. + // Alternatively this is also called from MockInitiator::~MockInitiator(). + EXPECT_EQ(mocked_i2c.Finalize(), OkStatus()); \ No newline at end of file
diff --git a/pw_i2c/initiator_mock.cc b/pw_i2c/initiator_mock.cc new file mode 100644 index 0000000..b0d2997 --- /dev/null +++ b/pw_i2c/initiator_mock.cc
@@ -0,0 +1,65 @@ +// Copyright 2020 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#include "pw_i2c/initiator_mock.h" + +#include <iostream> + +#include "pw_assert/check.h" + +namespace pw::i2c { + +Status MockInitiator::DoWriteReadFor( + Address device_address, + ConstByteSpan tx_buffer, + ByteSpan rx_buffer, + chrono::SystemClock::duration for_at_least) { + PW_CHECK_INT_LT(expected_transaction_index_, expected_transactions_.size()); + + EXPECT_EQ( + expected_transactions_[expected_transaction_index_].address().GetTenBit(), + device_address.GetTenBit()); + + auto expected_for_at_least = + expected_transactions_[expected_transaction_index_].for_at_least(); + if (expected_for_at_least.has_value()) { + EXPECT_EQ(expected_for_at_least.value(), for_at_least); + } + + ConstByteSpan expected_tx_buffer = + expected_transactions_[expected_transaction_index_].write_buffer(); + EXPECT_TRUE(std::equal(expected_tx_buffer.begin(), + expected_tx_buffer.end(), + tx_buffer.begin(), + tx_buffer.end())); + + ConstByteSpan expected_rx_buffer = + expected_transactions_[expected_transaction_index_].read_buffer(); + EXPECT_EQ(expected_rx_buffer.size(), rx_buffer.size()); + + std::copy( + expected_rx_buffer.begin(), expected_rx_buffer.end(), rx_buffer.begin()); + + // Do not directly return this value as expected_transaction_index_ should be + // incremented. + const Status expected_return_value = + expected_transactions_[expected_transaction_index_].return_value(); + + expected_transaction_index_ += 1; + + return expected_return_value; +} + +MockInitiator::~MockInitiator() { EXPECT_EQ(Finalize(), OkStatus()); } + +} // namespace pw::i2c \ No newline at end of file
diff --git a/pw_i2c/initiator_mock_test.cc b/pw_i2c/initiator_mock_test.cc new file mode 100644 index 0000000..beed22f --- /dev/null +++ b/pw_i2c/initiator_mock_test.cc
@@ -0,0 +1,108 @@ +// Copyright 2021 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +#include "pw_i2c/initiator_mock.h" + +#include <array> +#include <chrono> +#include <span> + +#include "gtest/gtest.h" +#include "pw_bytes/array.h" +#include "pw_bytes/span.h" +#include "pw_i2c/address.h" + +using namespace std::literals::chrono_literals; + +namespace pw::i2c { +namespace { + +TEST(Transaction, Read) { + static constexpr Address kAddress1 = Address::SevenBit<0x01>(); + constexpr auto kExpectRead1 = bytes::Array<1, 2, 3, 4, 5>(); + + static constexpr Address kAddress2 = Address::SevenBit<0x02>(); + constexpr auto kExpectRead2 = bytes::Array<3, 4, 5>(); + + auto expected_transactions = MakeExpectedTransactionArray( + {ReadTransaction(OkStatus(), kAddress1, kExpectRead1, 2ms), + ReadTransaction(OkStatus(), kAddress2, kExpectRead2, 2ms)}); + + MockInitiator mocked_i2c(expected_transactions); + + std::array<std::byte, kExpectRead1.size()> read1; + EXPECT_EQ(mocked_i2c.ReadFor(kAddress1, read1, 2ms), OkStatus()); + EXPECT_TRUE(std::equal( + read1.begin(), read1.end(), kExpectRead1.begin(), kExpectRead1.end())); + + std::array<std::byte, kExpectRead2.size()> read2; + EXPECT_EQ(mocked_i2c.ReadFor(kAddress2, read2, 2ms), OkStatus()); + EXPECT_TRUE(std::equal( + read2.begin(), read2.end(), kExpectRead2.begin(), kExpectRead2.end())); + + EXPECT_EQ(mocked_i2c.Finalize(), OkStatus()); +} + +TEST(Transaction, Write) { + static constexpr Address kAddress1 = Address::SevenBit<0x01>(); + constexpr auto kExpectWrite1 = bytes::Array<1, 2, 3, 4, 5>(); + + static constexpr Address kAddress2 = Address::SevenBit<0x02>(); + constexpr auto kExpectWrite2 = bytes::Array<3, 4, 5>(); + + auto expected_transactions = MakeExpectedTransactionArray( + {WriteTransaction(OkStatus(), kAddress1, kExpectWrite1, 2ms), + WriteTransaction(OkStatus(), kAddress2, kExpectWrite2, 2ms)}); + + MockInitiator mocked_i2c(expected_transactions); + + EXPECT_EQ(mocked_i2c.WriteFor(kAddress1, kExpectWrite1, 2ms), OkStatus()); + + EXPECT_EQ(mocked_i2c.WriteFor(kAddress2, kExpectWrite2, 2ms), OkStatus()); + + EXPECT_EQ(mocked_i2c.Finalize(), OkStatus()); +} + +TEST(Transaction, WriteRead) { + static constexpr Address kAddress1 = Address::SevenBit<0x01>(); + constexpr auto kExpectWrite1 = bytes::Array<1, 2, 3, 4, 5>(); + constexpr auto kExpectRead1 = bytes::Array<1, 2>(); + + static constexpr Address kAddress2 = Address::SevenBit<0x02>(); + constexpr auto kExpectWrite2 = bytes::Array<3, 4, 5>(); + constexpr const auto kExpectRead2 = bytes::Array<3, 4>(); + + auto expected_transactions = MakeExpectedTransactionArray({ + Transaction(OkStatus(), kAddress1, kExpectWrite1, kExpectRead1, 2ms), + Transaction(OkStatus(), kAddress2, kExpectWrite2, kExpectRead2, 2ms), + }); + + MockInitiator mocked_i2c(expected_transactions); + + std::array<std::byte, kExpectRead1.size()> read1; + EXPECT_EQ(mocked_i2c.WriteReadFor(kAddress1, kExpectWrite1, read1, 2ms), + OkStatus()); + EXPECT_TRUE(std::equal(read1.begin(), read1.end(), kExpectRead1.begin())); + + std::array<std::byte, kExpectRead1.size()> read2; + EXPECT_EQ(mocked_i2c.WriteReadFor(kAddress2, kExpectWrite2, read2, 2ms), + OkStatus()); + EXPECT_TRUE(std::equal( + read2.begin(), read2.end(), kExpectRead2.begin(), kExpectRead2.end())); + + EXPECT_EQ(mocked_i2c.Finalize(), OkStatus()); +} + +} // namespace +} // namespace pw::i2c \ No newline at end of file
diff --git a/pw_i2c/public/pw_i2c/initiator_mock.h b/pw_i2c/public/pw_i2c/initiator_mock.h new file mode 100644 index 0000000..d7dd094 --- /dev/null +++ b/pw_i2c/public/pw_i2c/initiator_mock.h
@@ -0,0 +1,150 @@ +// Copyright 2020 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once +#include <array> +#include <cstddef> +#include <optional> + +#include "gtest/gtest.h" +#include "initiator.h" +#include "pw_bytes/span.h" + +namespace pw::i2c { + +// Represents a complete parameter set for the Initiator::DoWriteReadFor(). +class Transaction { + public: + // Same set of parameters as Initiator::DoWriteReadFor(), with the exception + // of optional parameter for_at_least. + constexpr Transaction( + Status expected_return_value, + Address device_address, + ConstByteSpan write_buffer, + ConstByteSpan read_buffer, + std::optional<chrono::SystemClock::duration> for_at_least = std::nullopt) + : return_value_(expected_return_value), + read_buffer_(read_buffer), + write_buffer_(write_buffer), + address_(device_address), + for_at_least_(for_at_least) {} + + // Gets the buffer that is virtually read. + ConstByteSpan read_buffer() const { return read_buffer_; } + + // Gets the buffer that should be written by the driver. + ConstByteSpan write_buffer() const { return write_buffer_; } + + // Gets the min duration for a blocking i2c transaction. + std::optional<chrono::SystemClock::duration> for_at_least() const { + return for_at_least_; + } + + // Gets the i2c address that the i2c transaction is targetting. + Address address() const { return address_; } + + // Gets the expected return value. + Status return_value() const { return return_value_; } + + private: + const Status return_value_; + const ConstByteSpan read_buffer_; + const ConstByteSpan write_buffer_; + const Address address_; + const std::optional<chrono::SystemClock::duration> for_at_least_; +}; + +// Read transaction is a helper that constructs a read only transaction. +constexpr Transaction ReadTransaction( + Status expected_return_value, + Address device_address, + ConstByteSpan read_buffer, + std::optional<chrono::SystemClock::duration> for_at_least = std::nullopt) { + return std::move(Transaction(expected_return_value, + device_address, + ConstByteSpan(), + read_buffer, + for_at_least)); +} + +// WriteTransaction is a helper that constructs a write only transaction. +constexpr Transaction WriteTransaction( + Status expected_return_value, + Address device_address, + ConstByteSpan write_buffer, + std::optional<chrono::SystemClock::duration> for_at_least = std::nullopt) { + return std::move(Transaction(expected_return_value, + device_address, + write_buffer, + ConstByteSpan(), + for_at_least)); +} + +// MockInitiator takes a series of read and/or write transactions and +// compares them against user/driver input. +// +// This mock uses Gtest to ensure that the transactions instantiated meet +// expectations. This MockedInitiator should be instantiated inside a Gtest test +// frame. +class MockInitiator : public Initiator { + public: + explicit constexpr MockInitiator(std::span<Transaction> transaction_list) + : expected_transactions_(transaction_list), + expected_transaction_index_(0) {} + + // Should be called at the end of the test to ensure that all expected + // transactions have been met. + // Returns: + // Ok - Success. + // OutOfRange - The mocked set of transactions has not been exhausted. + Status Finalize() const { + if (expected_transaction_index_ != expected_transactions_.size()) { + return Status::OutOfRange(); + } + return Status(); + } + + // Runs Finalize() regardless of whether it was already optionally finalized. + ~MockInitiator(); + + private: + // Implements a mocked backend for the i2c initiator. + // + // Expects (via Gtest): + // tx_buffer == expected_transaction_tx_buffer + // tx_buffer.size() == expected_transaction_tx_buffer.size() + // rx_buffer.size() == expected_transaction_rx_buffer.size() + // + // Asserts: + // When the number of calls to this method exceed the number of expected + // transactions. + // + // Returns: + // Specified transaction return type + Status DoWriteReadFor(Address device_address, + ConstByteSpan tx_buffer, + ByteSpan rx_buffer, + chrono::SystemClock::duration for_at_least) override; + + std::span<Transaction> expected_transactions_; + size_t expected_transaction_index_; +}; + +// Makes a new i2c transactions list. +template <size_t size> +constexpr std::array<Transaction, size> MakeExpectedTransactionArray( + const Transaction (&transactions)[size]) { + return std::to_array(transactions); +} + +} // namespace pw::i2c \ No newline at end of file