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