hermes: card_qrtr: Unit test framework added
This change introduces a unit test framework for the CardQrtr
implementation. The framework allows tests to fake sending raw data
from the modem to the host with:
ModemReceiveData(raw_data);
The framework also allows tests to set up expectations for data that the
CardQrtr implementation will send to the modem, with the use of:
EXPECT_SEND(data, len);
BUG=chromium:847619
TEST=`cros_workon_make --board cheza hermes --test` passes.
Change-Id: Ibbc0d79525eb22d8067073d00802bee0f07222e6
Reviewed-on: https://chromium-review.googlesource.com/1368987
Commit-Ready: ChromeOS CL Exonerator Bot <chromiumos-cl-exonerator@appspot.gserviceaccount.com>
Tested-by: Alex Khouderchah <akhouderchah@chromium.org>
Reviewed-by: Eric Caruso <ejcaruso@chromium.org>
diff --git a/hermes/BUILD.gn b/hermes/BUILD.gn
index cdc5bd5..04f65a7 100644
--- a/hermes/BUILD.gn
+++ b/hermes/BUILD.gn
@@ -65,6 +65,7 @@
]
sources = [
"apdu_test.cc",
+ "card_qrtr_test.cc",
]
deps = [
":libhermes",
diff --git a/hermes/card_qrtr.h b/hermes/card_qrtr.h
index f13a8f2..0279b4c 100644
--- a/hermes/card_qrtr.h
+++ b/hermes/card_qrtr.h
@@ -96,6 +96,8 @@
lpa::util::Executor* executor() override { return executor_; }
private:
+ friend class CardQrtrTest;
+
///////////////////
// State Diagram //
///////////////////
diff --git a/hermes/card_qrtr_test.cc b/hermes/card_qrtr_test.cc
new file mode 100644
index 0000000..c4461e5
--- /dev/null
+++ b/hermes/card_qrtr_test.cc
@@ -0,0 +1,330 @@
+// Copyright 2018 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 "hermes/card_qrtr.h"
+
+#include <memory>
+#include <utility>
+
+#include <base/bind.h>
+#include <base/files/scoped_file.h>
+#include <base/strings/string_number_conversions.h>
+#include <fcntl.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "hermes/apdu.h"
+#include "hermes/sgp_22.h"
+#include "hermes/socket_qrtr.h"
+#include "hermes/type_traits.h"
+#include "hermes/utils.h"
+
+//
+// General testing structure
+// -------------------------
+// The CardQrtr implementation sends and receives data from a qrtr socket, whose
+// other end is a modem. In order to fake communication with the modem, the
+// qrtr socket is replaced with a regular file descriptor, with the modem itself
+// being faked by the CardQrtrTest testing framework.
+//
+// For each TEST_F(CardQrtrTest, ...) test, sending data from modem -> CardQrtr
+// can be faked with CardQrtrTest::CardReceiveData(...). The CardQrtr -> modem
+// messages are obviously not faked, as it is what we are testing, but
+// CardQrtr::SendApdus is now wrapped by CardQrtrTest::SendApdus. The
+// EXPECT_SEND macro is used to verify that the sent data is as we expected. In
+// both cases, the transaction IDs of provided data is ignored, and the proper
+// transaction ID values from the calls made to CardQrtr::AllocateIds are used
+// instead. This means that tests will not break if the implementation of
+// AllocateIds is changed.
+//
+
+using ::testing::_;
+using ::testing::ElementsAreArray;
+using ::testing::Invoke;
+using ::testing::WithArgs;
+
+namespace {
+
+constexpr uint32_t kTestNode = 0;
+constexpr uint32_t kTestPort = 59;
+const char* kQrtrFilename = "/tmp/hermes_qrtr_test";
+
+constexpr auto kQrtrNewServerResp = hermes::make_array<uint8_t>(
+ 0x04, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x3B, 0x00, 0x00, 0x00
+);
+
+constexpr auto kQrtrResetReq = hermes::make_array<uint8_t>(
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+);
+
+constexpr auto kQrtrResetResp = hermes::make_array<uint8_t>(
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00,
+ 0x00
+);
+
+constexpr auto kQrtrOpenLogicalChannelReq = hermes::make_array<uint8_t>(
+ 0x00, 0x00, 0x00, 0x42, 0x00, 0x18, 0x00, 0x01, 0x01, 0x00, 0x01, 0x10, 0x11,
+ 0x00, 0x10, 0xA0, 0x00, 0x00, 0x05, 0x59, 0x10, 0x10, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x89, 0x00, 0x00, 0x01, 0x00
+);
+
+constexpr auto kQrtrOpenLogicalChannelResp = hermes::make_array<uint8_t>(
+ 0x02, 0x00, 0x00, 0x42, 0x00, 0x35, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x12, 0x22, 0x00, 0x21, 0x6F, 0x1F, 0x84, 0x10, 0xA0, 0x00, 0x00, 0x05,
+ 0x59, 0x10, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0x89, 0x00, 0x00, 0x01, 0x00, 0xA5,
+ 0x04, 0x9F, 0x65, 0x01, 0xFF, 0xE0, 0x05, 0x82, 0x03, 0x02, 0x00, 0x00, 0x11,
+ 0x02, 0x00, 0x90, 0x00, 0x10, 0x01, 0x00, 0x01
+);
+
+constexpr auto kApduPrefix = hermes::make_array<uint8_t>(
+ 0x00, 0x00, 0x00, 0x3B, 0x00, 0x13, 0x00, 0x01, 0x01, 0x00, 0x01, 0x02, 0x08,
+ 0x00, 0x06, 0x00, 0x80, 0xE2, 0x91, 0x00, 0x00
+);
+
+constexpr auto kApduSuffix = hermes::make_array<uint8_t>(
+ 0x10, 0x01, 0x00, 0x01
+);
+
+constexpr auto kGetChallengeApdu = hermes::make_array<uint8_t>(
+ 0xBF, 0x2E, 0x00
+);
+
+constexpr auto kGetChallengeResp = hermes::make_array<uint8_t>(
+ 0x02, 0x00, 0x00, 0x3B, 0x00, 0x23, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x10, 0x19, 0x00, 0x17, 0x00, 0xBF, 0x2E, 0x12, 0x80, 0x10, 0x5A, 0x6C,
+ 0x23, 0x71, 0x94, 0xBE, 0xAB, 0x24, 0xF4, 0xEF, 0xAB, 0x54, 0xB7, 0x3A, 0x59,
+ 0xCF, 0x90, 0x00
+);
+
+void NullResponseCallback(std::vector<std::vector<uint8_t>>& responses, int err) {}
+
+// Create a full QRTR packet given the data of an APDU message. The current
+// implementation only works for non-fragmented APDUs.
+template <typename Iterator>
+hermes::EnableIfIterator_t<Iterator, std::vector<uint8_t>> CreateQrtrFromApdu(
+ Iterator first, Iterator last) {
+ std::vector<uint8_t> result;
+ result.insert(result.end(), kApduPrefix.begin(), kApduPrefix.end());
+ result.insert(result.end(), first, last);
+ result.insert(result.end(), kApduSuffix.begin(), kApduSuffix.end());
+ result[5] = result.size() - 7;
+ result[12] = result.size() - 18;
+ result[14] = std::distance(first, last) + 5;
+ result[20] = std::distance(first, last);
+ return result;
+}
+
+} // namespace
+
+// Expect CardQrtr instance to send the provided vector of data to the modem.
+// This macro should only be called within a CardQrtrTest.
+//
+// Note that the transaction ID in the provided vector will be ignored and the
+// earliest unused ID from CardQrtr::AllocateId will be used instead. This
+// allows for changes in message ordering to not invalidate the data passed
+// to this macro.
+#define EXPECT_SEND(socket_obj, data) \
+ EXPECT_CALL(socket_obj, Send(_, data.size(), _)) \
+ .Times(1) \
+ .WillOnce(WithArgs<0, 1>(Invoke( \
+ [this, d = data](const void* arr, size_t l) { \
+ auto expected = d; \
+ expected[1] = ((uint8_t*)arr)[1]; \
+ this->receive_ids_.push_back(expected[1]); \
+ EXPECT_THAT(expected, ElementsAreArray((uint8_t*)arr, l)); \
+ return 0; \
+ })))
+
+namespace hermes {
+
+// Socket class which mocks the outgoing (host -> modem) socket calls and
+// provides implementations for incoming (modem -> host) socket calls that reads
+// data from kQrtrFilename rather than from an actual QRTR socket.
+class MockSocketQrtr : public SocketInterface {
+ public:
+ void SetDataAvailableCallback(DataAvailableCallback cb) override {
+ cb_ = cb;
+ }
+
+ bool Open() override {
+ socket_ = base::ScopedFD(open(kQrtrFilename, O_RDWR));
+ if (!socket_.is_valid()) {
+ return false;
+ }
+ int off = lseek(socket_.get(), 0, SEEK_SET);
+ EXPECT_EQ(off, 0);
+ // Return without setting up a MessageLoop::WatchFileDescriptor. The epoll
+ // syscall does not always support regular file descriptors. Libevent could
+ // be configured not to use epoll, but this would require modifying or
+ // substituting base::MessagePumpLibevent. Instead, CardQrtrTest will
+ // manually call the DataAvailableCallback as needed.
+ return true;
+ }
+
+ bool IsValid() const override { return socket_.is_valid(); }
+ Type GetType() const override { return Type::kQrtr; }
+
+ int Recv(void* buf, size_t size, void* metadata) override {
+ int bytes_read = read(socket_.get(), buf, size);
+ EXPECT_EQ(bytes_read, size);
+ LOG(INFO) << "Mock CardQrtr receiving data (" << size
+ << " bytes): " << base::HexEncode(buf, size);
+
+ if (metadata) {
+ auto data = reinterpret_cast<SocketQrtr::PacketMetadata*>(metadata);
+ data->node = kTestNode;
+ if (size && *static_cast<uint8_t*>(buf) == QRTR_TYPE_NEW_SERVER) {
+ data->port = QRTR_PORT_CTRL;
+ } else {
+ data->port = kTestPort;
+ }
+ }
+ return bytes_read;
+ }
+
+ MOCK_METHOD0(Close, void());
+ MOCK_METHOD3(StartService, bool(uint32_t, uint16_t, uint16_t));
+ MOCK_METHOD3(StopService, bool(uint32_t, uint16_t, uint16_t));
+ MOCK_METHOD3(Send, int(const void*, size_t, const void*));
+
+ private:
+ friend class CardQrtrTest;
+
+ base::ScopedFD socket_;
+ DataAvailableCallback cb_;
+};
+
+// Test framework for CardQrtr tests. Allows for the faking of modem -> cpu
+// responses with the use of CardReceiveData.
+class CardQrtrTest : public testing::Test {
+ public:
+ CardQrtrTest() {
+ auto socket = std::make_unique<MockSocketQrtr>();
+ socket_ = socket.get();
+ card_ = CardQrtr::Create(std::move(socket), nullptr, nullptr);
+ }
+
+ protected:
+ // Fake modem initialization such that tests may jump right to sending QMI
+ // commands
+ void SetUp() override {
+ fd_.reset(open(kQrtrFilename, O_RDWR | O_CREAT | O_TRUNC, 0777));
+ ASSERT_TRUE(fd_.is_valid());
+
+ receive_ids_.clear();
+ }
+
+ void TearDown() override {
+ EXPECT_CALL(*socket_, Close());
+ card_.reset(nullptr);
+ fd_.reset();
+ }
+
+ // Wrapper for CardQrtr::SendApdus. Tests should use this rather than
+ // CardQrtr::SendApdus.
+ void SendApdus(std::vector<lpa::card::Apdu> commands,
+ CardQrtr::ResponseCallback cb) {
+ EXPECT_CALL(*socket_, StartService(_, _, _))
+ // Add a receive transaction id when new_lookup is called.
+ .WillOnce(WithoutArgs(Invoke([this](){
+ this->receive_ids_.push_back(0);
+ return true; })));
+
+ {
+ ::testing::InSequence dummy;
+
+ // Expect RESET and OPEN_LOGICAL_CHANNEL request after receiving
+ // NEW_SERVER.
+ EXPECT_SEND(*socket_, kQrtrResetReq);
+ EXPECT_SEND(*socket_, kQrtrOpenLogicalChannelReq);
+ }
+
+ card_->SendApdus(std::move(commands), std::move(cb));
+ SimulateInitialization();
+
+ EXPECT_CALL(*socket_, StopService(_, _, _));
+ }
+
+ // Cause |card_| to receive the provided data.
+ template <typename Iterator>
+ EnableIfIterator_t<Iterator, void> CardReceiveData(
+ Iterator first, Iterator last) {
+ std::vector<uint8_t> receive_data(first, last);
+ receive_data[1] = receive_ids_[0];
+ receive_ids_.pop_front();
+
+ int ret = write(fd_.get(), receive_data.data(), receive_data.size());
+ EXPECT_EQ(ret, receive_data.size());
+ // Set card buffer size so that the proper amount of data is read from fd.
+ card_->buffer_.resize(receive_data.size());
+ socket_->cb_.Run(card_->socket_.get());
+ }
+
+ void SimulateInitialization() {
+ // Receive NEW_SERVER response from sock_new_lookup
+ CardReceiveData(kQrtrNewServerResp.begin(), kQrtrNewServerResp.end());
+ // Receive RESET response from RESET request
+ CardReceiveData(kQrtrResetResp.begin(), kQrtrResetResp.end());
+ // Receive repsonse to OPEN_LOGICAL_CHANNEL request
+ CardReceiveData(kQrtrOpenLogicalChannelResp.begin(),
+ kQrtrOpenLogicalChannelResp.end());
+ }
+
+ base::ScopedFD fd_;
+ // Queue of transaction ids created by the CardQrtr instance in question. This
+ // is used such that AllocateId implementations may change without breaking
+ // the unit tests (which should not be affected by changes in id allocation
+ // strategy). Send ids are ids to use when sending commands. Likewise for
+ // receive ids.
+ std::deque<uint16_t> receive_ids_;
+ MockSocketQrtr* socket_;
+ std::unique_ptr<CardQrtr> card_;
+};
+
+///////////
+// TESTS //
+///////////
+
+TEST_F(CardQrtrTest, EmptyApdu) {
+ auto v = std::vector<uint8_t>();
+ EXPECT_SEND(*socket_, CreateQrtrFromApdu(v.begin(), v.end()));
+ std::vector<lpa::card::Apdu> commands = {lpa::card::Apdu::NewStoreData({})};
+ SendApdus(std::move(commands), NullResponseCallback);
+}
+
+TEST_F(CardQrtrTest, RequestGetEid) {
+ EXPECT_SEND(*socket_, CreateQrtrFromApdu(kGetChallengeApdu.begin(),
+ kGetChallengeApdu.end()));
+ std::vector<lpa::card::Apdu> commands = {lpa::card::Apdu::NewStoreData(
+ std::vector<uint8_t>(kGetChallengeApdu.begin(),
+ kGetChallengeApdu.end()))};
+ SendApdus(std::move(commands), NullResponseCallback);
+}
+
+TEST_F(CardQrtrTest, SendTwoApdus) {
+ auto v = std::vector<uint8_t>();
+ {
+ ::testing::InSequence dummy;
+
+ EXPECT_SEND(*socket_, CreateQrtrFromApdu(kGetChallengeApdu.begin(),
+ kGetChallengeApdu.end()));
+ // Do not expect to reinitialize the modem in between APDUs.
+ EXPECT_SEND(*socket_, CreateQrtrFromApdu(v.begin(), v.end()));
+ }
+
+ std::vector<lpa::card::Apdu> commands = {
+ lpa::card::Apdu::NewStoreData(
+ std::vector<uint8_t>(kGetChallengeApdu.begin(),
+ kGetChallengeApdu.end())),
+ lpa::card::Apdu::NewStoreData({})};
+ SendApdus(std::move(commands), NullResponseCallback);
+ CardReceiveData(kGetChallengeResp.begin(),
+ kGetChallengeResp.end());
+}
+
+} // namespace hermes
diff --git a/hermes/type_traits.h b/hermes/type_traits.h
new file mode 100644
index 0000000..4dc7c4f
--- /dev/null
+++ b/hermes/type_traits.h
@@ -0,0 +1,52 @@
+// Copyright 2018 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.
+
+#ifndef HERMES_TYPE_TRAITS_H_
+#define HERMES_TYPE_TRAITS_H_
+
+#include <type_traits>
+#include <vector>
+
+namespace hermes_internal {
+
+template<typename... Ts> struct make_void { typedef void type; };
+template<typename... Ts> using void_t = typename make_void<Ts...>::type;
+
+template <typename T>
+struct is_array : std::false_type {};
+
+template <typename T, std::size_t N>
+struct is_array<std::array<T, N>> : std::true_type {};
+
+template <typename T>
+struct is_vector : std::false_type {};
+
+template <typename T, typename Allocator>
+struct is_vector<std::vector<T, Allocator>> : std::true_type {};
+
+template <typename, typename = void>
+struct is_iterator : std::false_type {};
+
+template <typename T>
+struct is_iterator<T, void_t<
+ typename std::iterator_traits<T>::iterator_category>> : std::true_type {};
+
+} // namespace hermes_internal
+
+namespace hermes {
+
+template <typename T, typename ReturnType>
+using EnableIfArrayOrVector_t =
+ std::enable_if_t<hermes_internal::is_vector<T>::value ||
+ hermes_internal::is_array<T>::value,
+ ReturnType>;
+
+template <typename T, typename ReturnType>
+using EnableIfIterator_t =
+ std::enable_if_t<hermes_internal::is_iterator<T>::value,
+ ReturnType>;
+
+} // namespace hermes
+
+#endif // HERMES_TYPE_TRAITS_H_
diff --git a/hermes/utils.h b/hermes/utils.h
new file mode 100644
index 0000000..9b3347d
--- /dev/null
+++ b/hermes/utils.h
@@ -0,0 +1,25 @@
+// Copyright 2018 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.
+
+#ifndef HERMES_UTILS_H_
+#define HERMES_UTILS_H_
+
+#include <utility>
+
+namespace hermes {
+
+// Create a std::array from a set of values without manually specifying the
+// size of the array. Note that unlike the make_array likely to make its way
+// into C++20, this function always requires the user to specify ElementType.
+// This is done so that users are not surprised by the element type of resulting
+// arrays when std::common_type is used.
+template <typename ElementType, typename... T>
+constexpr auto make_array(T&&... values) {
+ return std::array<ElementType, sizeof...(T)>{
+ static_cast<ElementType>(std::forward<T>(values))...};
+}
+
+} // namespace hermes
+
+#endif // HERMES_UTILS_H_