pw_kvs: Add flash partition test

Add unit tests for testing flash partition. Put the main test
implementation in to a common file that is used by end tests that
provide the actual partition to test.

Add configuration define PW_FLASH_MAX_FLASH_ALIGNMENT that is
used to size flash write buffers.

Change-Id: Ib3dd2381037d15bd61552184f59769074dece44f
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/13220
Commit-Queue: David Rogers <davidrogers@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
diff --git a/pw_kvs/BUILD b/pw_kvs/BUILD
index a1fd8b8..dcf7f66 100644
--- a/pw_kvs/BUILD
+++ b/pw_kvs/BUILD
@@ -38,6 +38,7 @@
         "public/pw_kvs/internal/key_descriptor.h",
         "public/pw_kvs/internal/sectors.h",
         "public/pw_kvs/internal/span_traits.h",
+        "pw_kvs_private/config.h",
         "pw_kvs_private/macros.h",
         "sectors.cc",
     ],
@@ -93,6 +94,21 @@
 )
 
 pw_cc_library(
+    name = "flash_partition_test",
+    srcs = [
+        "flash_partition_test.cc",
+    ],
+    hdrs = [
+        "public/pw_kvs/flash_partition_test.h",
+    ],
+    deps = [
+        ":pw_kvs",
+        "//pw_log",
+        "//pw_span",
+    ],
+)
+
+pw_cc_library(
     name = "test_utils",
     hdrs = [
         "pw_kvs_private/byte_utils.h",
@@ -170,6 +186,17 @@
 )
 
 pw_cc_test(
+    name = "fake_flash_partition_test",
+    srcs = ["fake_flash_partition_test.cc"],
+    deps = [
+        ":pw_kvs",
+        ":flash_partition_test",
+        "//pw_log:backend",
+        "//pw_unit_test",
+    ],
+)
+
+pw_cc_test(
     name = "key_value_store_test",
     srcs = ["key_value_store_test.cc"],
     deps = [
diff --git a/pw_kvs/BUILD.gn b/pw_kvs/BUILD.gn
index 0b0c9f4..bb380e3 100644
--- a/pw_kvs/BUILD.gn
+++ b/pw_kvs/BUILD.gn
@@ -46,6 +46,7 @@
     "public/pw_kvs/internal/key_descriptor.h",
     "public/pw_kvs/internal/sectors.h",
     "public/pw_kvs/internal/span_traits.h",
+    "pw_kvs_private/config.h",
     "pw_kvs_private/macros.h",
     "sectors.cc",
   ]
@@ -83,6 +84,17 @@
   deps = [ dir_pw_log ]
 }
 
+pw_source_set("flash_partition_test") {
+  public = [ "public/pw_kvs/flash_partition_test.h" ]
+  sources = [ "flash_partition_test.cc" ]
+  deps = [
+    dir_pw_kvs,
+    dir_pw_log,
+    dir_pw_span,
+    dir_pw_unit_test,
+  ]
+}
+
 pw_source_set("test_utils") {
   public_configs = [ ":default_config" ]
   public = [ "pw_kvs_private/byte_utils.h" ]
@@ -107,6 +119,7 @@
     ":checksum_test",
     ":entry_test",
     ":entry_cache_test",
+    ":fake_flash_partition_test",
     ":key_value_store_test",
     ":key_value_store_binary_format_test",
     ":key_value_store_fuzz_test",
@@ -149,6 +162,15 @@
   sources = [ "entry_cache_test.cc" ]
 }
 
+pw_test("fake_flash_partition_test") {
+  deps = [
+    ":fake_flash",
+    ":flash_partition_test",
+    dir_pw_log,
+  ]
+  sources = [ "fake_flash_partition_test.cc" ]
+}
+
 pw_test("key_value_store_test") {
   deps = [
     ":crc16",
diff --git a/pw_kvs/entry.cc b/pw_kvs/entry.cc
index 3343548..cbc07d7 100644
--- a/pw_kvs/entry.cc
+++ b/pw_kvs/entry.cc
@@ -19,11 +19,19 @@
 #include <cinttypes>
 #include <cstring>
 
+#include "pw_kvs_private/config.h"
 #include "pw_kvs_private/macros.h"
 #include "pw_log/log.h"
 
 namespace pw::kvs::internal {
 
+static_assert(
+    kMaxFlashAlignment >= Entry::kMinAlignmentBytes,
+    "Flash alignment is required to be at least Entry::kMinAlignmentBytes");
+
+constexpr size_t kWriteBufferSize =
+    std::max(kMaxFlashAlignment, 4 * Entry::kMinAlignmentBytes);
+
 using std::byte;
 using std::string_view;
 
@@ -93,11 +101,11 @@
 StatusWithSize Entry::Write(string_view key,
                             std::span<const byte> value) const {
   FlashPartition::Output flash(partition(), address_);
-  return AlignedWrite<64>(flash,
-                          alignment_bytes(),
-                          {std::as_bytes(std::span(&header_, 1)),
-                           std::as_bytes(std::span(key)),
-                           value});
+  return AlignedWrite<kWriteBufferSize>(flash,
+                                        alignment_bytes(),
+                                        {std::as_bytes(std::span(&header_, 1)),
+                                         std::as_bytes(std::span(key)),
+                                         value});
 }
 
 Status Entry::Update(const EntryFormat& new_format,
@@ -122,7 +130,7 @@
                transaction_id());
 
   FlashPartition::Output output(partition(), new_address);
-  AlignedWriterBuffer<4 * kMinAlignmentBytes> writer(alignment_bytes(), output);
+  AlignedWriterBuffer<kWriteBufferSize> writer(alignment_bytes(), output);
 
   // Use this object's header rather than the header in flash of flash, since
   // this Entry may have been updated.
diff --git a/pw_kvs/fake_flash_partition_test.cc b/pw_kvs/fake_flash_partition_test.cc
new file mode 100644
index 0000000..8b28011
--- /dev/null
+++ b/pw_kvs/fake_flash_partition_test.cc
@@ -0,0 +1,61 @@
+// 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 "gtest/gtest.h"
+#include "pw_kvs/fake_flash_memory.h"
+#include "pw_kvs/flash_memory.h"
+#include "pw_kvs/flash_partition_test.h"
+#include "pw_log/log.h"
+
+namespace pw::kvs::PartitionTest {
+namespace {
+
+TEST(FakeFlashPartitionTest, FillTest16) {
+  FakeFlashMemoryBuffer<512, 8> flash(16);
+  FlashPartition test_partition(&flash);
+
+  // WriteTest(test_partition);
+}
+
+TEST(FakeFlashPartitionTest, FillTest64) {
+  FakeFlashMemoryBuffer<512, 8> flash(64);
+  FlashPartition test_partition(&flash);
+
+  // WriteTest(test_partition);
+}
+
+TEST(FakeFlashPartitionTest, FillTest256) {
+  FakeFlashMemoryBuffer<512, 8> flash(256);
+  FlashPartition test_partition(&flash);
+
+  // WriteTest(test_partition);
+}
+
+TEST(FakeFlashPartitionTest, EraseTest) {
+  FakeFlashMemoryBuffer<512, 8> flash(16);
+  FlashPartition test_partition(&flash);
+
+  // EraseTest(test_partition);
+}
+
+TEST(FakeFlashPartitionTest, ReadOnlyTest) {
+  FakeFlashMemoryBuffer<512, 8> flash(16);
+  FlashPartition test_partition(
+      &flash, 0, flash.sector_count(), 0, PartitionPermission::kReadOnly);
+
+  // ReadOnlyTest(test_partition);
+}
+
+}  // namespace
+}  // namespace pw::kvs::PartitionTest
diff --git a/pw_kvs/flash_memory.cc b/pw_kvs/flash_memory.cc
index 30af38f..0e8af99 100644
--- a/pw_kvs/flash_memory.cc
+++ b/pw_kvs/flash_memory.cc
@@ -20,6 +20,7 @@
 #include <cinttypes>
 #include <cstring>
 
+#include "pw_kvs_private/config.h"
 #include "pw_kvs_private/macros.h"
 #include "pw_log/log.h"
 #include "pw_status/status_with_size.h"
@@ -66,22 +67,25 @@
 Status FlashPartition::IsRegionErased(Address source_flash_address,
                                       size_t length,
                                       bool* is_erased) {
-  // Max alignment is artificial to keep the stack usage low for this
-  // function. Using 16 because it's the alignment of encrypted flash.
-  constexpr size_t kMaxAlignment = 16;
-
   // Relying on Read() to check address and len arguments.
   if (is_erased == nullptr) {
     return Status::INVALID_ARGUMENT;
   }
+
+  // TODO(pwbug/214): Currently using a single flash alignment to do both the
+  // read and write. The allowable flash read length may be less than what write
+  // needs (possibly by a bunch), resulting in buffer and erased_pattern_buffer
+  // being bigger than they need to be.
   const size_t alignment = alignment_bytes();
-  if (alignment > kMaxAlignment || kMaxAlignment % alignment ||
+  if (alignment > kMaxFlashAlignment || kMaxFlashAlignment % alignment ||
       length % alignment) {
     return Status::INVALID_ARGUMENT;
   }
 
-  byte buffer[kMaxAlignment];
-  byte erased_pattern_buffer[kMaxAlignment];
+  byte buffer[kMaxFlashAlignment];
+
+  // TODO(pwrev/215): Stop using erased_pattern_buffer to save stack.
+  byte erased_pattern_buffer[kMaxFlashAlignment];
 
   size_t offset = 0;
   std::memset(erased_pattern_buffer,
diff --git a/pw_kvs/flash_partition_test.cc b/pw_kvs/flash_partition_test.cc
new file mode 100644
index 0000000..6b5a9ce
--- /dev/null
+++ b/pw_kvs/flash_partition_test.cc
@@ -0,0 +1,168 @@
+// 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_kvs/flash_partition_test.h"
+
+#include <span>
+
+#include "gtest/gtest.h"
+#include "pw_kvs/flash_memory.h"
+#include "pw_kvs_private/config.h"
+#include "pw_log/log.h"
+
+namespace pw::kvs::PartitionTest {
+
+constexpr size_t kTestDataSize = kMaxFlashAlignment;
+
+namespace {
+
+void WriteData(FlashPartition& partition, uint8_t fill_byte) {
+  uint8_t test_data[kTestDataSize];
+  memset(test_data, fill_byte, sizeof(test_data));
+
+  ASSERT_GE(kTestDataSize, partition.alignment_bytes());
+
+  partition.Erase(0, partition.sector_count());
+
+  const size_t chunks_per_sector =
+      partition.sector_size_bytes() / partition.alignment_bytes();
+
+  // Fill partition sector by sector. Fill the sector with an integer number
+  // of alignment-size chunks. If the sector is not evenly divisible by
+  // alignment-size, the remainder is not written.
+  for (size_t sector_index = 0; sector_index < partition.sector_count();
+       sector_index++) {
+    FlashPartition::Address address =
+        sector_index * partition.sector_size_bytes();
+
+    for (size_t chunk_index = 0; chunk_index < chunks_per_sector;
+         chunk_index++) {
+      StatusWithSize status = partition.Write(
+          address, as_bytes(std::span(test_data, partition.alignment_bytes())));
+      ASSERT_EQ(Status::OK, status.status());
+      ASSERT_EQ(partition.alignment_bytes(), status.size());
+      address += partition.alignment_bytes();
+    }
+  }
+
+  // Check the fill result. Use expect so the test doesn't bail on error.
+  // Count the errors and print if any errors are found.
+  size_t error_count = 0;
+  for (size_t sector_index = 0; sector_index < partition.sector_count();
+       sector_index++) {
+    FlashPartition::Address address =
+        sector_index * partition.sector_size_bytes();
+
+    for (size_t chunk_index = 0; chunk_index < chunks_per_sector;
+         chunk_index++) {
+      memset(test_data, 0, sizeof(test_data));
+      StatusWithSize status =
+          partition.Read(address, partition.alignment_bytes(), test_data);
+
+      EXPECT_EQ(Status::OK, status.status());
+      EXPECT_EQ(partition.alignment_bytes(), status.size());
+      if (!status.ok() || (partition.alignment_bytes() != status.size())) {
+        error_count++;
+        continue;
+      }
+
+      for (size_t i = 0; i < partition.alignment_bytes(); i++) {
+        if (test_data[i] != fill_byte) {
+          error_count++;
+        }
+      }
+
+      address += partition.alignment_bytes();
+    }
+  }
+
+  EXPECT_EQ(error_count, 0U);
+  if (error_count != 0) {
+    PW_LOG_ERROR("Partition test, fill '%c', %u errors found",
+                 fill_byte,
+                 unsigned(error_count));
+  }
+}
+
+}  // namespace
+
+void WriteTest(FlashPartition& partition, size_t test_iterations) {
+  ASSERT_GE(kTestDataSize, partition.alignment_bytes());
+
+  for (size_t i = 0; i < test_iterations; i++) {
+    WriteData(partition, 0);
+    WriteData(partition, 0xff);
+    WriteData(partition, 0x55);
+    WriteData(partition, 0xa3);
+  }
+}
+
+void EraseTest(FlashPartition& partition) {
+  static const uint8_t fill_byte = 0x55;
+  uint8_t test_data[kTestDataSize];
+  memset(test_data, fill_byte, sizeof(test_data));
+
+  ASSERT_GE(kTestDataSize, partition.alignment_bytes());
+
+  const size_t block_size =
+      std::min(sizeof(test_data), partition.sector_size_bytes());
+  auto data_span = std::span(test_data, block_size);
+
+  ASSERT_EQ(Status::OK, partition.Erase(0, partition.sector_count()));
+
+  // Write to the first page of each sector.
+  for (size_t sector_index = 0; sector_index < partition.sector_count();
+       sector_index++) {
+    FlashPartition::Address address =
+        sector_index * partition.sector_size_bytes();
+
+    StatusWithSize status = partition.Write(address, as_bytes(data_span));
+    ASSERT_EQ(Status::OK, status.status());
+    ASSERT_EQ(block_size, status.size());
+  }
+
+  ASSERT_EQ(Status::OK, partition.Erase());
+
+  bool is_erased;
+  ASSERT_EQ(Status::OK,
+            partition.IsRegionErased(0, partition.size_bytes(), &is_erased));
+  ASSERT_EQ(true, is_erased);
+
+  // Read the first page of each sector and make sure it has been erased.
+  for (size_t sector_index = 0; sector_index < partition.sector_count();
+       sector_index++) {
+    FlashPartition::Address address =
+        sector_index * partition.sector_size_bytes();
+
+    StatusWithSize status =
+        partition.Read(address, data_span.size_bytes(), data_span.data());
+    ASSERT_EQ(Status::OK, status.status());
+    ASSERT_EQ(data_span.size_bytes(), status.size());
+
+    ASSERT_EQ(true, partition.AppearsErased(as_bytes(data_span)));
+  }
+}
+
+void ReadOnlyTest(FlashPartition& partition) {
+  uint8_t test_data[kTestDataSize];
+  auto data_span = std::span(test_data);
+
+  ASSERT_EQ(Status::PERMISSION_DENIED,
+            partition.Erase(0, partition.sector_count()));
+
+  ASSERT_EQ(Status::PERMISSION_DENIED,
+            partition.Write(0, as_bytes(data_span)).status());
+}
+
+}  // namespace pw::kvs::PartitionTest
diff --git a/pw_kvs/key_value_store_test.cc b/pw_kvs/key_value_store_test.cc
index 35840ef..3549e22 100644
--- a/pw_kvs/key_value_store_test.cc
+++ b/pw_kvs/key_value_store_test.cc
@@ -157,7 +157,7 @@
 // Although it might be useful to test other configurations, some tests require
 // at least 3 sectors; therfore it should have this when checked in.
 FakeFlashMemoryBuffer<4 * 1024, 6> test_flash(
-    16);  // 4 x 4k sectors, 16 byte alignment
+    16);  // 6 x 4k sectors, 16 byte alignment
 FlashPartition test_partition(&test_flash, 0, test_flash.sector_count());
 FakeFlashMemoryBuffer<1024, 60> large_test_flash(8);
 FlashPartition large_test_partition(&large_test_flash,
@@ -1332,9 +1332,7 @@
 class LargeEmptyInitializedKvs : public ::testing::Test {
  protected:
   LargeEmptyInitializedKvs() : kvs_(&large_test_partition, default_format) {
-    ASSERT_EQ(
-        Status::OK,
-        large_test_partition.Erase(0, large_test_partition.sector_count()));
+    ASSERT_EQ(Status::OK, large_test_partition.Erase());
     ASSERT_EQ(Status::OK, kvs_.Init());
   }
 
diff --git a/pw_kvs/public/pw_kvs/flash_memory.h b/pw_kvs/public/pw_kvs/flash_memory.h
index ac8f429..46be588 100644
--- a/pw_kvs/public/pw_kvs/flash_memory.h
+++ b/pw_kvs/public/pw_kvs/flash_memory.h
@@ -117,7 +117,7 @@
  private:
   const uint32_t sector_size_;
   const uint32_t flash_sector_count_;
-  const uint8_t alignment_;
+  const uint32_t alignment_;
   const uint32_t start_address_;
   const uint32_t start_sector_;
   const std::byte erased_memory_content_;
diff --git a/pw_kvs/public/pw_kvs/flash_partition_test.h b/pw_kvs/public/pw_kvs/flash_partition_test.h
new file mode 100644
index 0000000..8c8496d
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/flash_partition_test.h
@@ -0,0 +1,28 @@
+// 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 <cstddef>
+
+#include "pw_kvs/flash_memory.h"
+
+namespace pw::kvs::PartitionTest {
+
+void WriteTest(FlashPartition& partition, size_t test_iterations = 2);
+
+void EraseTest(FlashPartition& partition);
+
+void ReadOnlyTest(FlashPartition& partition);
+
+}  // namespace pw::kvs::PartitionTest
diff --git a/pw_kvs/pw_kvs_private/config.h b/pw_kvs/pw_kvs_private/config.h
new file mode 100644
index 0000000..0976bc0
--- /dev/null
+++ b/pw_kvs/pw_kvs_private/config.h
@@ -0,0 +1,29 @@
+// 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.
+
+// Utilities for building std::byte arrays from strings or integer values.
+#pragma once
+
+#include <cstddef>
+
+#ifndef PW_KVS_MAX_FLASH_ALIGNMENT
+#define PW_KVS_MAX_FLASH_ALIGNMENT 256UL
+#endif
+
+static_assert((PW_KVS_MAX_FLASH_ALIGNMENT >= 16UL),
+              "Max flash alignment is required to be at least 16");
+
+namespace pw::kvs {
+inline constexpr size_t kMaxFlashAlignment = PW_KVS_MAX_FLASH_ALIGNMENT;
+}  // namespace pw::kvs