[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