[sw] Add mock_mmio.h, for unit-testing DIFs.
Signed-off-by: Miguel Young de la Sota <mcyoung@google.com>
diff --git a/sw/device/lib/base/mmio.h b/sw/device/lib/base/mmio.h
index 2fcae5b..8369486 100644
--- a/sw/device/lib/base/mmio.h
+++ b/sw/device/lib/base/mmio.h
@@ -5,11 +5,28 @@
#ifndef OPENTITAN_SW_DEVICE_LIB_BASE_MMIO_H_
#define OPENTITAN_SW_DEVICE_LIB_BASE_MMIO_H_
+// This file is included in C and C++, and, as such, needs to be marked as
+// extern "C" in C++ to make sure linking works out.
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/**
+ * Memory-mapped IO functions, which either map to volatile accesses, or can be
+ * replaced with instrumentation calls at compile time, for use with tests.
+ *
+ * Compiling translation units that pull in this header with |-DMOCK_MMIO| will
+ * disable the definitions of |mmio_region_read| and |mmio_region_write|. These
+ * symbols can then be defined by a test harness to allow for instrumentation of
+ * MMIO accesses.
+ */
+
+#ifndef MOCK_MMIO
+/**
* An mmio_region_t is an opaque handle to an MMIO region; it should only be
* modified using the functions provided in this header.
*/
@@ -119,6 +136,28 @@
uint32_t value) {
((volatile uint32_t *)base.base)[offset / sizeof(uint32_t)] = value;
}
+#else // MOCK_MMIO
+/**
+ * "Instrumented" mmio_region_t.
+ *
+ * Instead of containing a volatile pointer, mmio_region_t becomes a |void *|
+ * when |-DMOCK_MMIO| is enabled. This makes it incompatible with the non-mock
+ * version of |mmio_region_t|, which prevents users from being able to access
+ * the pointer inside.
+ */
+typedef struct mmio_region { void *mock; } mmio_region_t;
+
+/**
+ * Stubbed-out read/write operations for overriding by a testing library.
+ */
+uint8_t mmio_region_read8(mmio_region_t base, ptrdiff_t offset);
+uint16_t mmio_region_read16(mmio_region_t base, ptrdiff_t offset);
+uint32_t mmio_region_read32(mmio_region_t base, ptrdiff_t offset);
+
+void mmio_region_write8(mmio_region_t base, ptrdiff_t offset, uint8_t value);
+void mmio_region_write16(mmio_region_t base, ptrdiff_t offset, uint16_t value);
+void mmio_region_write32(mmio_region_t base, ptrdiff_t offset, uint32_t value);
+#endif // MOCK_MMIO
/**
* Reads the bits in |mask| from the MMIO region |base| at the given offset.
@@ -226,4 +265,8 @@
mmio_region_nonatomic_set_mask32(base, offset, 0x1, bit_index);
}
+#ifdef __cplusplus
+} // extern "C"
+#endif // __cplusplus
+
#endif // OPENTITAN_SW_DEVICE_LIB_BASE_MMIO_H_
diff --git a/sw/device/lib/meson.build b/sw/device/lib/meson.build
index f6a31a5..0766ad4 100644
--- a/sw/device/lib/meson.build
+++ b/sw/device/lib/meson.build
@@ -4,8 +4,8 @@
subdir('base')
subdir('runtime')
-subdir('dif')
subdir('testing')
+subdir('dif')
# UART library (sw_lib_uart)
sw_lib_uart = declare_dependency(
diff --git a/sw/device/lib/testing/meson.build b/sw/device/lib/testing/meson.build
index 0ef4a1c..72ff645 100644
--- a/sw/device/lib/testing/meson.build
+++ b/sw/device/lib/testing/meson.build
@@ -4,7 +4,7 @@
sw_lib_testing_gtest_src_dir = meson.source_root() / 'sw/vendor/google_googletest'
sw_lib_testing_gtest_lock_file = meson.source_root() / 'sw/vendor/google_googletest.lock.hjson'
-# Build dir below is $REPO_TOP/build-out/sw/fpga/sw/device/lib/testing/google_googletest
+# Build dir below is $REPO_TOP/build-out/sw/${DEVICE}/sw/device/lib/testing/google_googletest
sw_lib_testing_gtest_build_dir = meson.current_build_dir() / 'google_googletest'
sw_lib_testing_empty_file_for_dep = 'empty_file_for_googletest_dependency.cc'
@@ -25,21 +25,16 @@
echo "Done!"
'''.format(sw_lib_testing_gtest_build_dir, sw_lib_testing_gtest_src_dir, sw_lib_testing_empty_file_for_dep)
-build_gtest = custom_target(
+sw_lib_testing_build_gtest = custom_target(
'googletest',
output: sw_lib_testing_empty_file_for_dep,
depend_files: sw_lib_testing_gtest_lock_file,
- command: ['bash', '-c', build_gtest_cmd],
+ command: ['bash', '-e', '-c', build_gtest_cmd],
+ console: true,
)
-gtest_inc = include_directories(
- '../../../vendor/google_googletest/googletest/include',
- '../../../vendor/google_googletest/googlemock/include',
-)
-
-gtest = declare_dependency(
- sources: [build_gtest],
- include_directories: gtest_inc,
+sw_lib_testing_gtest = declare_dependency(
+ sources: [sw_lib_testing_build_gtest],
link_args: [
'-L' + sw_lib_testing_gtest_build_dir / 'lib',
'-lgmock',
@@ -47,4 +42,37 @@
'-lgtest',
],
dependencies: dependency('threads'),
+ compile_args: [
+ # These are necessary in order to make gtest headers correctly find other
+ # gtest headers.
+ '-I' + meson.source_root() / 'sw/vendor/google_googletest/googletest/include',
+ '-I' + meson.source_root() / 'sw/vendor/google_googletest/googlemock/include',
+ ],
)
+
+sw_lib_testing_mock_mmio = declare_dependency(
+ link_with: static_library(
+ 'mock_mmio',
+ sources: [
+ meson.source_root() / 'sw/device/lib/base/mmio.c',
+ 'mock_mmio.cc',
+ ],
+ dependencies: [sw_lib_testing_gtest],
+ native: true,
+ c_args: ['-DMOCK_MMIO'],
+ cpp_args: ['-DMOCK_MMIO'],
+ )
+)
+
+# Example test using mock_mmio.h, which also serves to
+# test that mock_mmio.h works correctly.
+test('mock_mmio_test', executable(
+ 'mock_mmio_test',
+ sources: ['mock_mmio_test.cc'],
+ dependencies: [
+ sw_lib_testing_gtest,
+ sw_lib_testing_mock_mmio,
+ ],
+ native: true,
+ cpp_args: ['-DMOCK_MMIO'],
+))
diff --git a/sw/device/lib/testing/mock_mmio.cc b/sw/device/lib/testing/mock_mmio.cc
new file mode 100644
index 0000000..51a13cf
--- /dev/null
+++ b/sw/device/lib/testing/mock_mmio.cc
@@ -0,0 +1,42 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/lib/testing/mock_mmio.h"
+
+#include "sw/device/lib/base/mmio.h"
+
+namespace mock_mmio {
+// Definitions for the MOCK_MMIO-mode declarations in |mmio.h|.
+extern "C" {
+uint8_t mmio_region_read8(mmio_region_t base, ptrdiff_t offset) {
+ auto *dev = static_cast<MockDevice *>(base.mock);
+ return dev->Read8(offset);
+}
+
+uint16_t mmio_region_read16(mmio_region_t base, ptrdiff_t offset) {
+ auto *dev = static_cast<MockDevice *>(base.mock);
+ return dev->Read16(offset);
+}
+
+uint32_t mmio_region_read32(mmio_region_t base, ptrdiff_t offset) {
+ auto *dev = static_cast<MockDevice *>(base.mock);
+ return dev->Read32(offset);
+}
+
+void mmio_region_write8(mmio_region_t base, ptrdiff_t offset, uint8_t value) {
+ auto *dev = static_cast<MockDevice *>(base.mock);
+ dev->Write8(offset, value);
+}
+
+void mmio_region_write16(mmio_region_t base, ptrdiff_t offset, uint16_t value) {
+ auto *dev = static_cast<MockDevice *>(base.mock);
+ dev->Write16(offset, value);
+}
+
+void mmio_region_write32(mmio_region_t base, ptrdiff_t offset, uint32_t value) {
+ auto *dev = static_cast<MockDevice *>(base.mock);
+ dev->Write32(offset, value);
+}
+} // extern "C"
+} // namespace mock_mmio
\ No newline at end of file
diff --git a/sw/device/lib/testing/mock_mmio.h b/sw/device/lib/testing/mock_mmio.h
new file mode 100644
index 0000000..1866655
--- /dev/null
+++ b/sw/device/lib/testing/mock_mmio.h
@@ -0,0 +1,211 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENTITAN_SW_DEVICE_LIB_TESTING_MOCK_MMIO_H_
+#define OPENTITAN_SW_DEVICE_LIB_TESTING_MOCK_MMIO_H_
+
+#include <stdint.h>
+
+#include <memory>
+#include <vector>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "sw/device/lib/base/mmio.h"
+
+namespace mock_mmio {
+/**
+ * A MockDevice represents a mock implementation of an MMIO device.
+ *
+ * MockDevice provides two mockable member functions, representing a read and a
+ * write at a particular offset from the base address. This class can be
+ * converted into a |mmio_region_t| value, which, when used in |mmio.h|
+ * functions like |read32()|, will map to the appropriate mock member function
+ * calls.
+ *
+ * To maintain sequencing, |ReadN()| and |WriteN()| should not be
+ * |EXPECT_CALL|'ed directly; instead, |EXPECT_READN| and |EXPECT_WRITEN| should
+ * be used, instead.
+ *
+ * To use this class, |-DMOCK_MMIO| must be enabled in all translation units
+ * using |mmio.h|.
+ */
+class MockDevice {
+ public:
+ MockDevice() = default;
+
+ MockDevice(const MockDevice &) = delete;
+ MockDevice &operator=(const MockDevice &) = delete;
+ MockDevice(MockDevice &&) = delete;
+ MockDevice &operator=(MockDevice &&) = delete;
+
+ /**
+ * Converts this MockDevice into a mmio_region_t opaque object,
+ * which is compatible with |mmio.h| functions.
+ */
+ mmio_region_t region() { return {this}; }
+
+ MOCK_METHOD(uint8_t, Read8, (ptrdiff_t offset));
+ MOCK_METHOD(uint16_t, Read16, (ptrdiff_t offset));
+ MOCK_METHOD(uint32_t, Read32, (ptrdiff_t offset));
+
+ MOCK_METHOD(void, Write8, (ptrdiff_t offset, uint8_t value));
+ MOCK_METHOD(void, Write16, (ptrdiff_t offset, uint16_t value));
+ MOCK_METHOD(void, Write32, (ptrdiff_t offset, uint32_t value));
+};
+
+/**
+ * Conveninence fixture for creating device tests.
+ *
+ * This class should be derived by a test fixture (along with |testing::Test|)
+ * and used in a |TEST_F| block. Doing so will make the |EXPECT_READN| and
+ * |EXPECT_WRITEN| conveinence macros useable.
+ *
+ * The device being mocked can be accessed in the test body with |this->dev()|.
+ * |this->| is required in this case, since the name |dev| is not immediately
+ * visible.
+ */
+class MmioTest {
+ protected:
+ MockDevice &dev() { return *dev_; }
+
+ private:
+ std::unique_ptr<MockDevice> dev_ = std::make_unique<MockDevice>();
+ testing::InSequence seq_;
+};
+
+} // namespace mock_mmio
+
+/**
+ * Expect a read to the device |dev| at the given offset, returning the given
+ * 8-bit value.
+ *
+ * This expectation is sequenced with all other |EXPECT_READ| and |EXPECT_WRITE|
+ * calls.
+ */
+#define EXPECT_READ8_AT(dev, offset, value) \
+ EXPECT_CALL(dev, Read8(offset)).WillOnce(testing::Return(value))
+
+/**
+ * Expect a read to the device |dev| at the given offset, returning the given
+ * 16-bit value.
+ *
+ * This expectation is sequenced with all other |EXPECT_READ| and |EXPECT_WRITE|
+ * calls.
+ */
+#define EXPECT_READ16_AT(dev, offset, value) \
+ EXPECT_CALL(dev, Read16(offset)).WillOnce(testing::Return(value))
+
+/**
+ * Expect a read to the device |dev| at the given offset, returning the given
+ * 32-bit value.
+ *
+ * This expectation is sequenced with all other |EXPECT_READ| and |EXPECT_WRITE|
+ * calls.
+ */
+#define EXPECT_READ32_AT(dev, offset, value) \
+ EXPECT_CALL(dev, Read32(offset)).WillOnce(testing::Return(value))
+
+/**
+ * Expect a write to the device |dev| at the given offset with the given 8-bit
+ * value.
+ *
+ * This expectation is sequenced with all other |EXPECT_READ| and |EXPECT_WRITE|
+ * calls.
+ */
+#define EXPECT_WRITE8_AT(dev, offset, value) \
+ EXPECT_CALL(dev, Write8(offset, value))
+
+/**
+ * Expect a write to the device |dev| at the given offset with the given 16-bit
+ * value.
+ *
+ * This expectation is sequenced with all other |EXPECT_READ| and |EXPECT_WRITE|
+ * calls.
+ */
+#define EXPECT_WRITE16_AT(dev, offset, value) \
+ EXPECT_CALL(dev, Write16(offset, value))
+
+/**
+ * Expect a write to the device |dev| at the given offset with the given 32-bit
+ * value.
+ *
+ * This expectation is sequenced with all other |EXPECT_READ| and |EXPECT_WRITE|
+ * calls.
+ */
+#define EXPECT_WRITE32_AT(dev, offset, value) \
+ EXPECT_CALL(dev, Write32(offset, value))
+
+/**
+ * Expect a read at the given offset, returning the given 8-bit value.
+ *
+ * This function is only available in tests using a fixture that derives
+ * |DeviceTest|.
+ *
+ * This expectation is sequenced with all other |EXPECT_READ| and |EXPECT_WRITE|
+ * calls.
+ */
+#define EXPECT_READ8(offset, value) EXPECT_READ8_AT(this->dev(), offset, value)
+
+/**
+ * Expect a read at the given offset, returning the given 16-bit value.
+ *
+ * This function is only available in tests using a fixture that derives
+ * |DeviceTest|.
+ *
+ * This expectation is sequenced with all other |EXPECT_READ| and |EXPECT_WRITE|
+ * calls.
+ */
+#define EXPECT_READ16(offset, value) \
+ EXPECT_READ16_AT(this->dev(), offset, value)
+
+/**
+ * Expect a read at the given offset, returning the given 32-bit value.
+ *
+ * This function is only available in tests using a fixture that derives
+ * |DeviceTest|.
+ *
+ * This expectation is sequenced with all other |EXPECT_READ| and |EXPECT_WRITE|
+ * calls.
+ */
+#define EXPECT_READ32(offset, value) \
+ EXPECT_READ32_AT(this->dev(), offset, value)
+
+/**
+ * Expect a write to the given offset with the given 8-bit value.
+ *
+ * This function is only available in tests using a fixture that derives
+ * |DeviceTest|.
+ *
+ * This expectation is sequenced with all other |EXPECT_READ| and |EXPECT_WRITE|
+ * calls.
+ */
+#define EXPECT_WRITE8(offset, value) \
+ EXPECT_WRITE8_AT(this->dev(), offset, value);
+
+/**
+ * Expect a write to the given offset with the given 16-bit value.
+ *
+ * This function is only available in tests using a fixture that derives
+ * |DeviceTest|.
+ *
+ * This expectation is sequenced with all other |EXPECT_READ| and |EXPECT_WRITE|
+ * calls.
+ */
+#define EXPECT_WRITE16(offset, value) \
+ EXPECT_WRITE16_AT(this->dev(), offset, value);
+
+/**
+ * Expect a write to the given offset with the given 32-bit value.
+ *
+ * This function is only available in tests using a fixture that derives
+ * |DeviceTest|.
+ *
+ * This expectation is sequenced with all other |EXPECT_READ| and |EXPECT_WRITE|
+ * calls.
+ */
+#define EXPECT_WRITE32(offset, value) \
+ EXPECT_WRITE32_AT(this->dev(), offset, value);
+
+#endif // OPENTITAN_SW_DEVICE_LIB_TESTING_MOCK_MMIO_H_
diff --git a/sw/device/lib/testing/mock_mmio_test.cc b/sw/device/lib/testing/mock_mmio_test.cc
new file mode 100644
index 0000000..b232e02
--- /dev/null
+++ b/sw/device/lib/testing/mock_mmio_test.cc
@@ -0,0 +1,36 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/lib/testing/mock_mmio.h"
+
+#include "gtest/gtest.h"
+#include "sw/device/lib/base/mmio.h"
+
+namespace {
+using ::mock_mmio::MmioTest;
+using ::testing::Test;
+
+/**
+ * Exercises the register |dev| by reading a value at offset 0x0,
+ * writing its complement to 0x4, and then writing its upper half
+ * and lower half to 0x8 and 0xa.
+ */
+uint32_t WriteTwice(mmio_region_t dev) {
+ auto value = mmio_region_read32(dev, 0x0);
+ mmio_region_write32(dev, 0x4, ~value);
+ mmio_region_write16(dev, 0x8, value >> 16);
+ mmio_region_write16(dev, 0xa, value & 0xffff);
+ return value;
+}
+
+class WriteTwiceTest : public Test, public MmioTest {};
+TEST_F(WriteTwiceTest, WriteTwice) {
+ EXPECT_READ32(0x0, 0xdeadbeef);
+ EXPECT_WRITE32(0x4, 0x21524110)
+ EXPECT_WRITE16(0x8, 0xdead);
+ EXPECT_WRITE16(0xa, 0xbeef);
+
+ EXPECT_EQ(WriteTwice(dev().region()), 0xdeadbeef);
+}
+} // namespace