pw_string: StringBuilder utility class

StringBuilder can be used to flexibly and safely build strings in
fixed-size buffers.

Change-Id: Ie4453e1bcd11ef522bb0211304e9cf758ac17827
diff --git a/pw_string/BUILD b/pw_string/BUILD
index 3351fdc..544a1de 100644
--- a/pw_string/BUILD
+++ b/pw_string/BUILD
@@ -26,12 +26,15 @@
     name = "pw_string",
     srcs = [
         "format.cc",
+        "string_builder.cc",
         "type_to_string.cc",
     ],
     hdrs = [
         "public/pw_string/format.h",
+        "public/pw_string/string_builder.h",
         "public/pw_string/to_string.h",
         "public/pw_string/type_to_string.h",
+        "public/pw_string/util.h",
     ],
     deps = [
         "//pw_preprocessor",
@@ -56,6 +59,14 @@
 )
 
 pw_cc_test(
+    name = "string_builder_test",
+    srcs = ["string_builder_test.cc"],
+    deps = [
+        "//pw_string",
+    ],
+)
+
+pw_cc_test(
     name = "to_string_test",
     srcs = ["to_string_test.cc"],
     deps = [
diff --git a/pw_string/BUILD.gn b/pw_string/BUILD.gn
index 3221e02..1e5481b 100644
--- a/pw_string/BUILD.gn
+++ b/pw_string/BUILD.gn
@@ -28,9 +28,11 @@
     "public/pw_string/format.h",
     "public/pw_string/to_string.h",
     "public/pw_string/type_to_string.h",
+    "public/pw_string/util.h",
   ]
   sources = [
     "format.cc",
+    "string_builder.cc",
     "type_to_string.cc",
   ]
   sources += public
@@ -63,6 +65,16 @@
   ]
 }
 
+pw_test("string_builder_test") {
+  deps = [
+    ":pw_string",
+    "$dir_pw_unit_test:main",
+  ]
+  sources = [
+    "string_builder_test.cc",
+  ]
+}
+
 pw_test("to_string_test") {
   deps = [
     ":pw_string",
@@ -81,6 +93,16 @@
   ]
 }
 
+pw_test("util_test") {
+  deps = [
+    ":pw_string",
+    "$dir_pw_unit_test:main",
+  ]
+  sources = [
+    "util_test.cc",
+  ]
+}
+
 pw_size_report("format_size_report") {
   title = "Using pw::string::Format instead of snprintf"
 
diff --git a/pw_string/public/pw_string/string_builder.h b/pw_string/public/pw_string/string_builder.h
new file mode 100644
index 0000000..cb735b7
--- /dev/null
+++ b/pw_string/public/pw_string/string_builder.h
@@ -0,0 +1,422 @@
+// Copyright 2019 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 <algorithm>
+#include <cstdarg>
+#include <cstddef>
+#include <cstring>
+#include <string_view>
+#include <type_traits>
+#include <utility>
+
+#include "pw_preprocessor/compiler.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_string/to_string.h"
+
+namespace pw {
+
+// StringBuilder facilitates building formatted strings in a fixed-size buffer.
+// StringBuilders are always null terminated (unless they are constructed with
+// an empty buffer) and never overflow. Status is tracked for each operation and
+// an overall status is maintained, which reflects the most recent error.
+//
+// A StringBuilder does not own the buffer it writes to. It can be used to write
+// strings to any buffer. The StringBuffer template class, defined below,
+// allocates a buffer alongside a StringBuilder.
+//
+// StringBuilder supports C++-style << output, similar to std::ostringstream. It
+// also supports std::string-like append functions and printf-style output.
+//
+// StringBuilder uses the ToString function to support arbitrary types. Defining
+// a ToString overload in the pw::string namespace allows writing that type to a
+// StringBuilder with <<.
+//
+// For example, the following ToString overload allows writing MyStatus objects
+// to StringBuilders:
+//
+//   namespace pw::string {
+//
+//   StatusWithSize ToString(MyStatus value, const span<char>& buffer) {
+//     return CopyString(MyStatusString(value), buffer);
+//   }
+//
+//   }  // namespace pw::string
+//
+// For complex types, it may be easier to override StringBuilder's << operator,
+// similar to the standard library's std::ostream. For example:
+//
+//   namespace pw::string {
+//
+//   StringBuilder& operator<<(StringBuilder& sb, const MyType& value) {
+//     return sb << "MyType(" << value.foo << ", " << value.bar << ')';
+//   }
+//
+//   }  // namespace pw::string
+//
+// Alternately, complex types may use a StringBuilder in their ToString, but it
+// is likely to be simpler to override StringBuilder's operator<<.
+//
+// StringBuilder is safe, flexible, and results in much smaller code size than
+// using std::ostringstream. However, applications sensitive to code size should
+// use StringBuilder with care.
+//
+// The fixed code size cost of StringBuilder is significant, though smaller than
+// std::snprintf. Using StringBuilder's << and append methods exclusively in
+// place of snprintf reduces code size, but snprintf may be difficult to avoid.
+//
+// The incremental code size cost of StringBuilder is comparable to snprintf if
+// errors are handled. Each argument to StringBuilder's << expands to a function
+// call, but one or two StringBuilder appends may have a smaller code size
+// impact than a single snprintf call. See the size report for further analysis.
+class StringBuilder {
+ public:
+  // Creates an empty StringBuilder.
+  constexpr StringBuilder(const span<char>& buffer)
+      : buffer_(buffer), size_(0) {
+    NullTerminate();
+  }
+
+  // Disallow copy/assign to avoid confusion about where the string is actually
+  // stored. StringBuffers may be copied into one another.
+  StringBuilder(const StringBuilder&) = delete;
+
+  StringBuilder& operator=(const StringBuilder&) = delete;
+
+  // Returns the contents of the string buffer. Always null-terminated.
+  const char* data() const { return buffer_.data(); }
+  const char* c_str() const { return data(); }
+
+  // Returns a std::string_view of the contents of this StringBuilder. The
+  // std::string_view is invalidated if the StringBuilder contents change.
+  std::string_view view() const { return std::string_view(data(), size()); }
+
+  // Allow implicit conversions to std::string_view so StringBuilders can be
+  // passed into functions that take a std::string_view.
+  operator std::string_view() const { return view(); }
+
+  // Returns a span<const std::byte> representation of this StringBuffer.
+  span<const std::byte> as_bytes() const {
+    return span(reinterpret_cast<const std::byte*>(buffer_.data()), size_);
+  }
+
+  // Returns the StringBuilder's status, which reflects the most recent error
+  // that occurred while updating the string. After an update fails, the status
+  // remains non-OK until it is cleared with clear() or clear_status(). Returns:
+  //
+  //     OK if no errors have occurred
+  //     RESOURCE_EXHAUSTED if output to the StringBuilder was truncated
+  //     INVALID_ARGUMENT if printf-style formatting failed
+  //     OUT_OF_RANGE if an operation outside the buffer was attempted
+  //
+  Status status() const { return status_; }
+
+  // Returns status() and size() as a StatusWithSize.
+  StatusWithSize status_with_size() const {
+    return StatusWithSize(status_, size_);
+  }
+
+  // The status from the last operation. May be OK while status() is not OK.
+  Status last_status() const { return last_status_; }
+
+  // True if status() is Status::OK.
+  bool ok() const { return status_.ok(); }
+
+  // True if the string is empty.
+  bool empty() const { return size() == 0u; }
+
+  // Returns the current length of the string, excluding the null terminator.
+  size_t size() const { return size_; }
+
+  // Returns the maximum length of the string, excluding the null terminator.
+  size_t max_size() const { return buffer_.empty() ? 0u : buffer_.size() - 1; }
+
+  // Clears the string and resets its error state.
+  void clear();
+
+  // Sets the statuses to Status::OK;
+  void clear_status() {
+    status_ = Status::OK;
+    last_status_ = Status::OK;
+  }
+
+  // Appends a single character. Stets the status to RESOURCE_EXHAUSTED if the
+  // character cannot be added because the buffer is full.
+  void push_back(char ch) { append(1, ch); }
+
+  // Removes the last character. Sets the status to OUT_OF_RANGE if the buffer
+  // is empty.
+  void pop_back() { resize(size() - 1); }
+
+  // Appends the provided character count times.
+  StringBuilder& append(size_t count, char ch);
+
+  // Appends count characters from str to the end of the StringBuilder. If count
+  // exceeds the remaining space in the StringBuffer, max_size() - size()
+  // characters are appended and the status is set to RESOURCE_EXHAUSTED.
+  //
+  // str is not considered null-terminated and may contain null characters.
+  StringBuilder& append(const char* str, size_t count);
+
+  // Appends characters from the null-terminated string to the end of the
+  // StringBuilder. If the string's length exceeds the remaining space in the
+  // buffer, max_size() - size() characters are copied and the status is set to
+  // RESOURCE_EXHAUSTED.
+  //
+  // This function uses string::Length instead of std::strlen to avoid unbounded
+  // reads if the string is not null terminated.
+  StringBuilder& append(const char* str);
+
+  // Appends a std::string_view to the end of the StringBuilder.
+  StringBuilder& append(const std::string_view& str);
+
+  // Appends a substring from the std::string_view to the StringBuilder. Copies
+  // up to count characters starting from pos to the end of the StringBuilder.
+  // If pos > str.size(), sets the status to OUT_OF_RANGE.
+  StringBuilder& append(const std::string_view& str,
+                        size_t pos,
+                        size_t count = std::string_view::npos);
+
+  // Appends to the end of the StringBuilder using the << operator. This enables
+  // C++ stream-style formatted to StringBuilders.
+  template <typename T>
+  StringBuilder& operator<<(const T& value) {
+    // For std::string_view-compatible types, use the append function, which
+    // gives smaller code size.
+    if constexpr (std::is_convertible_v<T, std::string_view>) {
+      append(value);
+    } else {
+      HandleStatusWithSize(ToString(value, buffer_.subspan(size_)));
+    }
+    return *this;
+  }
+
+  // Provide a few additional operator<< overloads that reduce code size.
+  StringBuilder& operator<<(bool value) {
+    return append(value ? "true" : "false");
+  }
+
+  StringBuilder& operator<<(char value) {
+    push_back(value);
+    return *this;
+  }
+
+  StringBuilder& operator<<(std::nullptr_t) {
+    return append(string::kNullPointerString);
+  }
+
+  StringBuilder& operator<<(Status status) { return *this << status.str(); }
+
+  // Appends a printf-style string to the end of the StringBuilder. If the
+  // formatted string does not fit, the results are truncated and the status is
+  // set to RESOURCE_EXHAUSTED.
+  //
+  // Internally, calls string::Format, which calls std::vsnprintf.
+  PW_PRINTF_FORMAT(2, 3) StringBuilder& Format(const char* format, ...);
+
+  // Appends a vsnprintf-style string with va_list arguments to the end of the
+  // StringBuilder. If the formatted string does not fit, the results are
+  // truncated and the status is set to RESOURCE_EXHAUSTED.
+  //
+  // Internally, calls string::Format, which calls std::vsnprintf.
+  StringBuilder& Format(const char* format, va_list args);
+
+  // Sets the StringBuilder's size. This function only truncates; if
+  // new_size > size(), it sets status to OUT_OF_RANGE and does nothing.
+  void resize(size_t new_size);
+
+ protected:
+  // Functions to support StringBuffer copies.
+  constexpr StringBuilder(const span<char>& buffer, const StringBuilder& other)
+      : buffer_(buffer),
+        size_(other.size_),
+        status_(other.status_),
+        last_status_(other.last_status_) {}
+
+  void CopySizeAndStatus(const StringBuilder& other);
+
+ private:
+  size_t ResizeAndTerminate(size_t chars_to_append);
+
+  void HandleStatusWithSize(StatusWithSize written);
+
+  constexpr void NullTerminate() {
+    if (!buffer_.empty()) {
+      buffer_[size_] = '\0';
+    }
+  }
+
+  void SetErrorStatus(Status status);
+
+  const span<char> buffer_;
+
+  size_t size_;
+  Status status_;
+  Status last_status_;
+};
+
+// StringBuffers declare a buffer along with a StringBuilder. StringBuffer can
+// be used as a statically allocated replacement for std::ostringstream or
+// std::string. For example:
+//
+//   StringBuffer<32> str;
+//   str << "The answer is " << number << "!";  // with number = 42
+//   str.c_str();  // null terminated C string "The answer is 42."
+//   str.view();   // std::string_view of "The answer is 42."
+//
+template <size_t kSizeBytes>
+class StringBuffer : public StringBuilder {
+ public:
+  StringBuffer() : StringBuilder(buffer_) {}
+
+  // StringBuffers of the same size may be copied and assigned into one another.
+  StringBuffer(const StringBuffer& other) : StringBuilder(buffer_, other) {
+    CopyContents(other);
+  }
+
+  // A smaller StringBuffer may be copied or assigned into a larger one.
+  template <size_t kOtherSizeBytes>
+  StringBuffer(const StringBuffer<kOtherSizeBytes>& other)
+      : StringBuilder(buffer_, other) {
+    static_assert(StringBuffer<kOtherSizeBytes>::max_size() <= max_size(),
+                  "A StringBuffer cannot be copied into a smaller buffer");
+    CopyContents(other);
+  }
+
+  template <size_t kOtherSizeBytes>
+  StringBuffer& operator=(const StringBuffer<kOtherSizeBytes>& other) {
+    return assign<kOtherSizeBytes>(other);
+  }
+
+  StringBuffer& operator=(const StringBuffer& other) {
+    return assign<kSizeBytes>(other);
+  }
+
+  template <size_t kOtherSizeBytes>
+  StringBuffer& assign(const StringBuffer<kOtherSizeBytes>& other) {
+    static_assert(StringBuffer<kOtherSizeBytes>::max_size() <= max_size(),
+                  "A StringBuffer cannot be copied into a smaller buffer");
+    CopySizeAndStatus(other);
+    CopyContents(other);
+    return *this;
+  }
+
+  // Returns the maximum length of the string, excluding the null terminator.
+  static constexpr size_t max_size() { return kSizeBytes - 1; }
+
+  // Returns a StringBuffer<kSizeBytes>& instead of a generic StringBuilder& for
+  // append calls and stream-style operations.
+  template <typename... Args>
+  StringBuffer& append(Args&&... args) {
+    StringBuilder::append(std::forward<Args>(args)...);
+    return *this;
+  }
+
+  template <typename T>
+  StringBuffer& operator<<(T&& value) {
+    static_cast<StringBuilder&>(*this) << std::forward<T>(value);
+    return *this;
+  }
+
+ private:
+  template <size_t kOtherSize>
+  void CopyContents(const StringBuffer<kOtherSize>& other) {
+    std::memcpy(buffer_, other.data(), other.size() + 1);  // include the \0
+  }
+
+  static_assert(kSizeBytes >= 1u, "StringBuffers must be at least 1 byte long");
+  char buffer_[kSizeBytes];
+};
+
+namespace string_internal {
+
+// Internal code for determining the default size of StringBuffers created with
+// MakeString.
+//
+// StringBuffers created with MakeString default to at least 24 bytes. This is
+// large enough to fit the largest 64-bit integer (20 digits plus a \0), rounded
+// up to the nearest multiple of 4.
+inline constexpr size_t kDefaultMinimumStringBufferSize = 24;
+
+// By default, MakeString uses a buffer size large enough to fit all string
+// literal arguments. ArgLength uses this value as an estimate of the number of
+// characters needed to represent a non-string argument.
+inline constexpr size_t kDefaultArgumentSize = 4;
+
+// Returns a string literal's length or kDefaultArgumentSize for non-strings.
+template <typename T>
+constexpr size_t ArgLength() {
+  using Arg = std::remove_reference_t<T>;
+
+  // If the argument is an array of const char, assume it is a string literal.
+  if constexpr (std::is_array_v<Arg>) {
+    using Element = std::remove_reference_t<decltype(std::declval<Arg>()[0])>;
+
+    if constexpr (std::is_same_v<Element, const char>) {
+      return std::extent_v<Arg> > 0u ? std::extent_v<Arg> - 1 : size_t(0);
+    }
+  }
+
+  return kDefaultArgumentSize;
+}
+
+// This function returns the default string buffer size used by MakeString.
+template <typename... Args>
+constexpr size_t DefaultStringBufferSize() {
+  return std::max((size_t(1) + ... + ArgLength<Args>()),
+                  kDefaultMinimumStringBufferSize);
+}
+
+// Internal version of MakeString with const reference arguments instead of
+// deduced types, which include the lengths of string literals. Having this
+// function can reduce code size.
+template <size_t kBufferSize, typename... Args>
+auto InitializeStringBuffer(const Args&... args) {
+  return (StringBuffer<kBufferSize>() << ... << args);
+}
+
+}  // namespace string_internal
+
+// Makes a StringBuffer with a string version of a series of values. This is
+// useful for creating and initializing a StringBuffer or for conveniently
+// getting a null-terminated string. For example:
+//
+//     LOG_INFO("The MAC address is %s", MakeString(mac_address).c_str());
+//
+// By default, the buffer size is 24 bytes, large enough to fit any 64-bit
+// integer. If string literal arguments are provided, the default size will be
+// large enough to fit them and a null terminator, plus 4 additional bytes for
+// each argument. To use a fixed buffer size, set the kBufferSize template
+// argument. For example:
+//
+//   // Creates a default-size StringBuffer (10 + 10 + 4 + 1 + 1 = 26 bytes).
+//   auto sb = MakeString("1234567890", "1234567890", number, "!");
+//
+//   // Creates a 32-byte StringBuffer.
+//   auto sb = MakeString<32>("1234567890", "1234567890", number, "!");
+//
+// Keep in mind that each argument to MakeString expands to a function call.
+// MakeString may increase code size more than an equivalent pw::string::Format
+// (or std::snprintf) call.
+template <size_t kBufferSize = 0u, typename... Args>
+auto MakeString(Args&&... args) {
+  constexpr size_t kSize =
+      kBufferSize == 0u ? string_internal::DefaultStringBufferSize<Args...>()
+                        : kBufferSize;
+  return string_internal::InitializeStringBuffer<kSize>(args...);
+}
+
+}  // namespace pw
diff --git a/pw_string/public/pw_string/type_to_string.h b/pw_string/public/pw_string/type_to_string.h
index 4ce31df..b9bc52d 100644
--- a/pw_string/public/pw_string/type_to_string.h
+++ b/pw_string/public/pw_string/type_to_string.h
@@ -97,7 +97,11 @@
 // Writes a bool as "true" or "false". Semantics match CopyEntireString.
 StatusWithSize BoolToString(bool value, const span<char>& buffer);
 
-// Writes the pointer's address or "(null)". Semantics match CopyEntireString.
+// String used to represent null pointers.
+inline constexpr std::string_view kNullPointerString("(null)");
+
+// Writes the pointer's address or kNullPointerString. Semantics match
+// CopyEntireString.
 StatusWithSize PointerToString(const void* pointer, const span<char>& buffer);
 
 // Copies the string to the buffer, truncating if the full string does not fit.
diff --git a/pw_string/public/pw_string/util.h b/pw_string/public/pw_string/util.h
new file mode 100644
index 0000000..0375802
--- /dev/null
+++ b/pw_string/public/pw_string/util.h
@@ -0,0 +1,38 @@
+// Copyright 2019 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>
+
+namespace pw::string {
+
+// Calculates the length of a null-terminated string up to the specified maximum
+// length. If str is nullptr, returns 0.
+//
+// This function is a constexpr version of C11's strnlen_s.
+constexpr size_t Length(const char* str, size_t max_len) {
+  size_t length = 0;
+
+  if (str != nullptr) {
+    for (; length < max_len; ++length) {
+      if (str[length] == '\0') {
+        break;
+      }
+    }
+  }
+
+  return length;
+}
+
+}  // namespace pw::string
diff --git a/pw_string/string_builder.cc b/pw_string/string_builder.cc
new file mode 100644
index 0000000..4765126
--- /dev/null
+++ b/pw_string/string_builder.cc
@@ -0,0 +1,123 @@
+// Copyright 2019 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_string/string_builder.h"
+
+#include <cstdio>
+
+#include "pw_string/format.h"
+#include "pw_string/util.h"
+
+namespace pw {
+
+void StringBuilder::clear() {
+  size_ = 0;
+  NullTerminate();
+  status_ = Status::OK;
+  last_status_ = Status::OK;
+}
+
+StringBuilder& StringBuilder::append(size_t count, char ch) {
+  char* const append_destination = &buffer_[size_];
+  std::memset(append_destination, ch, ResizeAndTerminate(count));
+  return *this;
+}
+
+StringBuilder& StringBuilder::append(const char* str, size_t count) {
+  char* const append_destination = &buffer_[size_];
+  std::memcpy(append_destination, str, ResizeAndTerminate(count));
+  return *this;
+}
+
+StringBuilder& StringBuilder::append(const char* str) {
+  // Use buffer_.size() - size() as the maximum length so that strings too long
+  // to fit in the buffer will request one character too many, which sets the
+  // status to RESOURCE_EXHAUSTED.
+  return append(str, string::Length(str, buffer_.size() - size()));
+}
+
+StringBuilder& StringBuilder::append(const std::string_view& str) {
+  return append(str.data(), str.size());
+}
+
+StringBuilder& StringBuilder::append(const std::string_view& str,
+                                     size_t pos,
+                                     size_t count) {
+  if (pos > str.size()) {
+    SetErrorStatus(Status::OUT_OF_RANGE);
+    return *this;
+  }
+
+  return append(str.data() + pos, std::min(str.size() - pos, count));
+}
+
+size_t StringBuilder::ResizeAndTerminate(size_t chars_to_append) {
+  const size_t copied = std::min(chars_to_append, max_size() - size());
+  size_ += copied;
+  NullTerminate();
+
+  if (buffer_.empty() || chars_to_append != copied) {
+    SetErrorStatus(Status::RESOURCE_EXHAUSTED);
+  } else {
+    last_status_ = Status::OK;
+  }
+  return copied;
+}
+
+void StringBuilder::resize(size_t new_size) {
+  if (new_size <= size_) {
+    size_ = new_size;
+    NullTerminate();
+    last_status_ = Status::OK;
+  } else {
+    SetErrorStatus(Status::OUT_OF_RANGE);
+  }
+}
+
+StringBuilder& StringBuilder::Format(const char* format, ...) {
+  va_list args;
+  va_start(args, format);
+  Format(format, args);
+  va_end(args);
+
+  return *this;
+}
+
+StringBuilder& StringBuilder::Format(const char* format, va_list args) {
+  HandleStatusWithSize(string::Format(buffer_.subspan(size_), format, args));
+  return *this;
+}
+
+void StringBuilder::CopySizeAndStatus(const StringBuilder& other) {
+  size_ = other.size_;
+  status_ = other.status_;
+  last_status_ = other.last_status_;
+}
+
+void StringBuilder::HandleStatusWithSize(StatusWithSize written) {
+  const Status status = written.status();
+  last_status_ = status;
+  if (!status.ok()) {
+    status_ = status;
+  }
+
+  size_ += written.size();
+}
+
+void StringBuilder::SetErrorStatus(Status status) {
+  last_status_ = status;
+  status_ = status;
+}
+
+}  // namespace pw
diff --git a/pw_string/string_builder_test.cc b/pw_string/string_builder_test.cc
new file mode 100644
index 0000000..b2701e8
--- /dev/null
+++ b/pw_string/string_builder_test.cc
@@ -0,0 +1,569 @@
+// Copyright 2019 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_string//string_builder.h"
+
+#include <cinttypes>
+#include <cmath>
+#include <cstdint>
+#include <cstring>
+#include <string_view>
+
+#include "gtest/gtest.h"
+#include "pw_string/format.h"
+
+namespace {
+
+struct CustomType {
+  uint32_t a;
+  uint32_t b;
+
+  static constexpr const char* kToString = "This is a CustomType";
+
+  CustomType() = default;
+
+  // Non-copyable to verify StringBuffer's << operator doesn't copy it.
+  CustomType(const CustomType&) = delete;
+  CustomType& operator=(const CustomType&) = delete;
+};
+
+}  // namespace
+
+namespace pw {
+
+StatusWithSize ToString(const ::CustomType&, const span<char>& buffer) {
+  return string::Format(buffer, ::CustomType::kToString);
+}
+
+namespace {
+
+TEST(StringBuilder, EmptyBuffer_SizeAndMaxSizeAreCorrect) {
+  StringBuilder sb(span<char>{});
+
+  EXPECT_TRUE(sb.empty());
+  EXPECT_EQ(0u, sb.size());
+  EXPECT_EQ(0u, sb.max_size());
+}
+
+using namespace std::literals::string_view_literals;
+
+constexpr std::string_view kNoTouch = "DO NOT TOUCH\0VALUE SHOULD NOT CHANGE"sv;
+
+TEST(StringBuilder, EmptyBuffer_StreamOutput_WritesNothing) {
+  char buffer[kNoTouch.size()];
+  std::memcpy(buffer, kNoTouch.data(), sizeof(buffer));
+
+  StringBuilder sb(span(buffer, 0));
+
+  sb << CustomType() << " is " << 12345;
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.status());
+  EXPECT_EQ(kNoTouch, std::string_view(buffer, sizeof(buffer)));
+}
+
+TEST(StringBuilder, EmptyBuffer_Append_WritesNothing) {
+  char buffer[kNoTouch.size()];
+  std::memcpy(buffer, kNoTouch.data(), sizeof(buffer));
+
+  StringBuilder sb(span(buffer, 0));
+
+  EXPECT_FALSE(sb.append("Hello").ok());
+  EXPECT_EQ(kNoTouch, std::string_view(buffer, sizeof(buffer)));
+}
+
+TEST(StringBuilder, EmptyBuffer_Resize_WritesNothing) {
+  char buffer[kNoTouch.size()];
+  std::memcpy(buffer, kNoTouch.data(), sizeof(buffer));
+
+  StringBuilder sb(span(buffer, 0));
+
+  sb.resize(0);
+  EXPECT_TRUE(sb.ok());
+  EXPECT_EQ(kNoTouch, std::string_view(buffer, sizeof(buffer)));
+}
+
+TEST(StringBuilder, EmptyBuffer_AppendEmpty_ResourceExhausted) {
+  StringBuilder sb(span<char>{});
+  EXPECT_EQ(Status::OK, sb.last_status());
+  EXPECT_EQ(Status::OK, sb.status());
+
+  sb << "";
+
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.last_status());
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.status());
+}
+
+TEST(StringBuilder, Status_StartsOk) {
+  StringBuffer<16> sb;
+  EXPECT_EQ(Status::OK, sb.status());
+  EXPECT_EQ(Status::OK, sb.last_status());
+}
+
+TEST(StringBuilder, Status_StatusAndLastStatusUpdate) {
+  StringBuffer<16> sb;
+  sb << "Well, if only there were enough room in here for this string";
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.status());
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.last_status());
+
+  sb.resize(1029);
+  EXPECT_EQ(Status::OUT_OF_RANGE, sb.status());
+  EXPECT_EQ(Status::OUT_OF_RANGE, sb.last_status());
+
+  sb << "";
+  EXPECT_EQ(Status::OUT_OF_RANGE, sb.status());
+  EXPECT_EQ(Status::OK, sb.last_status());
+}
+
+TEST(StringBuilder, Status_ClearStatus_SetsStatuesToOk) {
+  StringBuffer<2> sb = MakeString<2>("Won't fit!!!!!");
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.status());
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.last_status());
+
+  sb.clear_status();
+  EXPECT_EQ(Status::OK, sb.status());
+  EXPECT_EQ(Status::OK, sb.last_status());
+}
+
+TEST(StringBuilder, StreamOutput_OutputSelf) {
+  auto sb = MakeString<32>("echo!");
+  sb << sb;
+
+  EXPECT_STREQ("echo!echo!", sb.data());
+  EXPECT_EQ(10u, sb.size());
+}
+
+TEST(StringBuilder, PushBack) {
+  StringBuffer<12> sb;
+  sb.push_back('?');
+  EXPECT_EQ(Status::OK, sb.last_status());
+  EXPECT_EQ(1u, sb.size());
+  EXPECT_STREQ("?", sb.data());
+}
+
+TEST(StringBuilder, PushBack_Full) {
+  StringBuffer<1> sb;
+  sb.push_back('!');
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.last_status());
+  EXPECT_EQ(0u, sb.size());
+}
+
+TEST(StringBuilder, PopBack) {
+  auto sb = MakeString<12>("Welcome!");
+  sb.pop_back();
+  EXPECT_EQ(Status::OK, sb.last_status());
+  EXPECT_EQ(7u, sb.size());
+  EXPECT_STREQ("Welcome", sb.data());
+}
+
+TEST(StringBuilder, PopBack_Empty) {
+  StringBuffer<12> sb;
+  sb.pop_back();
+  EXPECT_EQ(Status::OUT_OF_RANGE, sb.last_status());
+  EXPECT_EQ(0u, sb.size());
+}
+
+TEST(StringBuilder, Append_NonTerminatedString) {
+  static char bad_string[256];
+  std::memset(bad_string, '?', sizeof(bad_string));
+
+  StringBuffer<6> sb;
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.append(bad_string).last_status());
+  EXPECT_STREQ("?????", sb.data());
+}
+
+TEST(StringBuilder, Append_Chars) {
+  StringBuffer<8> sb;
+
+  EXPECT_TRUE(sb.append(7, '?').ok());
+  EXPECT_STREQ("???????", sb.data());
+}
+
+TEST(StringBuilder, Append_Chars_Full) {
+  StringBuffer<8> sb;
+
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.append(8, '?').last_status());
+  EXPECT_STREQ("???????", sb.data());
+}
+
+TEST(StringBuilder, Append_PartialCString) {
+  StringBuffer<12> sb;
+  EXPECT_TRUE(sb.append("123456", 4).ok());
+  EXPECT_EQ(4u, sb.size());
+  EXPECT_STREQ("1234", sb.data());
+}
+
+TEST(StringBuilder, Append_CString) {
+  auto sb = MakeString("hello");
+  EXPECT_TRUE(sb.append(" goodbye").ok());
+  EXPECT_STREQ("hello goodbye", sb.data());
+  EXPECT_EQ(13u, sb.size());
+}
+
+TEST(StringBuilder, Append_CString_Full) {
+  auto sb = MakeString<6>("hello");
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.append("890123", 1).last_status());
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.status());
+  EXPECT_EQ(sb.max_size(), sb.size());
+  EXPECT_STREQ("hello", sb.data());
+}
+
+TEST(StringBuilder, Append_StringView) {
+  auto sb = MakeString<32>("hello");
+  EXPECT_TRUE(sb.append("???"sv).ok());
+  EXPECT_EQ("hello???"sv, sb);
+}
+
+TEST(StringBuilder, Append_StringView_Substring) {
+  auto sb = MakeString<32>("I like ");
+  EXPECT_TRUE(sb.append("your shoes!!!"sv, 5, 5).ok());
+  EXPECT_EQ("I like shoes"sv, sb);
+}
+
+TEST(StringBuilder, Append_StringView_RemainingSubstring) {
+  auto sb = MakeString<32>("I like ");
+  EXPECT_TRUE(sb.append("your shoes!!!"sv, 5).ok());
+  EXPECT_EQ("I like shoes!!!"sv, sb);
+}
+
+TEST(StringBuilder, Resize_Smaller) {
+  auto sb = MakeString<12>("Four");
+  sb.resize(2);
+  EXPECT_TRUE(sb.ok());
+  EXPECT_EQ(2u, sb.size());
+  EXPECT_STREQ("Fo", sb.data());
+}
+
+TEST(StringBuilder, Resize_Clear) {
+  auto sb = MakeString<12>("Four");
+  sb.resize(0);
+  EXPECT_TRUE(sb.ok());
+  EXPECT_EQ(0u, sb.size());
+  EXPECT_STREQ("", sb.data());
+}
+
+TEST(StringBuilder, Resize_Larger_Fails) {
+  auto sb = MakeString<12>("Four");
+  EXPECT_EQ(4u, sb.size());
+  sb.resize(10);
+  EXPECT_EQ(sb.status(), Status::OUT_OF_RANGE);
+  EXPECT_EQ(4u, sb.size());
+}
+
+TEST(StringBuilder, Resize_LargerThanCapacity_Fails) {
+  auto sb = MakeString<12>("Four");
+  sb.resize(1234);
+  EXPECT_EQ(sb.status(), Status::OUT_OF_RANGE);
+  EXPECT_EQ(4u, sb.size());
+  EXPECT_STREQ("Four", sb.data());
+}
+
+TEST(StringBuilder, Format_Normal) {
+  StringBuffer<64> sb;
+  EXPECT_TRUE(sb.Format("0x%x", 0xabc).ok());
+  EXPECT_STREQ("0xabc", sb.data());
+
+  sb << "def";
+
+  EXPECT_TRUE(sb.Format("GHI").ok());
+  EXPECT_STREQ("0xabcdefGHI", sb.data());
+}
+
+TEST(StringBuilder, Format_ExhaustBuffer) {
+  StringBuffer<6> sb;
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.Format("012345").status());
+
+  EXPECT_STREQ("01234", sb.data());
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.status());
+}
+
+TEST(StringBuilder, StreamOutput_MultipleTypes) {
+  constexpr const char* kExpected = "This is -1true example\n of this";
+  constexpr const char* kExample = "example";
+
+  StringBuffer<64> sb;
+  sb << "This is " << -1 << true << ' ' << kExample << '\n' << " of this";
+
+  EXPECT_STREQ(kExpected, sb.data());
+  EXPECT_EQ(std::strlen(kExpected), sb.size());
+}
+
+TEST(StringBuilder, StreamOutput_FullBufferIgnoresExtraStrings) {
+  StringBuffer<6> sb;
+  EXPECT_EQ(5u, sb.max_size());  // max_size() excludes the null terminator
+
+  sb << 1 - 1;
+  EXPECT_TRUE(sb.ok());
+  EXPECT_STREQ("0", sb.data());
+
+  sb << true << "Now it's way " << static_cast<unsigned char>(2) << " long";
+  EXPECT_FALSE(sb.ok());
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.status());
+  EXPECT_STREQ("0true", sb.data());
+}
+
+TEST(StringBuilder, StreamOutput_ExhaustBuffer_InOneString) {
+  StringBuffer<9> sb;
+  EXPECT_EQ(8u, sb.max_size());
+
+  sb << "0123456789";  // write 10 chars
+  EXPECT_FALSE(sb.ok());
+  EXPECT_STREQ("01234567", sb.data());  // only can fit 8
+  EXPECT_EQ(8u, sb.size());
+
+  sb << "no"
+     << " more "
+     << "room" << '?';
+  EXPECT_STREQ("01234567", sb.data());
+}
+
+TEST(StringBuilder, StreamOutput_ExhaustBuffer_InTwoStrings) {
+  StringBuffer<4> sb;
+
+  sb << "01";  // fill 3/4 of buffer
+  EXPECT_EQ(2u, sb.size());
+  sb << "234";
+  EXPECT_STREQ("012", sb.data());
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.status());
+  EXPECT_EQ(3u, sb.size());
+}
+
+TEST(StringBuilder, StreamOutput_NonTerminatedString) {
+  static char bad_string[256];
+  std::memset(bad_string, '?', sizeof(bad_string));
+
+  StringBuffer<6> sb;
+  sb << "hey" << bad_string;
+
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.status());
+  EXPECT_STREQ("hey??", sb.data());
+}
+
+TEST(StringBuilder, SteamOutput_StringView) {
+  StringBuffer<6> buffer;
+  constexpr std::string_view hello("hello");
+
+  buffer << hello;
+  EXPECT_EQ(Status::OK, buffer.status());
+  EXPECT_STREQ("hello", buffer.data());
+}
+
+TEST(StringBuilder, StreamOutput_EmptyStringView) {
+  StringBuffer<4> buffer;
+  buffer << "hi" << std::string_view() << "!";
+  EXPECT_TRUE(buffer.ok());
+  EXPECT_STREQ("hi!", buffer.data());
+}
+
+TEST(StringBuffer, Assign) {
+  StringBuffer<10> one;
+  StringBuffer<10> two;
+
+  one << "What";
+  ASSERT_STREQ("What", one.data());
+  two = one;
+  EXPECT_STREQ("What", two.data());
+  EXPECT_NE(one.data(), two.data());
+  one << " the";
+  two << " heck";
+
+  EXPECT_STREQ("What the", one.data());
+  EXPECT_STREQ("What heck", two.data());
+
+  two << "0123456789";
+  ASSERT_STREQ("What heck", two.data());
+  ASSERT_EQ(Status::RESOURCE_EXHAUSTED, two.status());
+  ASSERT_EQ(Status::RESOURCE_EXHAUSTED, two.last_status());
+
+  one = two;
+  EXPECT_STREQ("What heck", one.data());
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, one.status());
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, one.last_status());
+
+  StringBuffer<12> three;
+  three = two;
+  EXPECT_STREQ(three.data(), two.data());
+  EXPECT_EQ(three.size(), two.size());
+}
+
+TEST(StringBuffer, CopyConstructFromSameSize) {
+  StringBuffer<10> one;
+
+  one << "What";
+  ASSERT_STREQ("What", one.data());
+  StringBuffer<10> two(one);
+  EXPECT_STREQ("What", two.data());
+  EXPECT_NE(one.data(), two.data());
+  one << " the";
+  two << " heck";
+
+  EXPECT_STREQ("What the", one.data());
+  EXPECT_STREQ("What heck", two.data());
+
+  two << "0123456789";
+  two << "";
+  ASSERT_STREQ("What heck", two.data());
+  ASSERT_EQ(Status::RESOURCE_EXHAUSTED, two.status());
+  ASSERT_EQ(Status::OK, two.last_status());
+}
+
+TEST(StringBuffer, CopyConstructFromSmaller) {
+  StringBuffer<10> one = MakeString<10>("You are the chosen one.");
+  StringBuffer<12> two(one);
+
+  EXPECT_STREQ("You are t", two.data());
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, two.status());
+}
+
+TEST(MakeString, Object) {
+  CustomType custom;
+  const auto sb = MakeString<64>(custom);
+
+  EXPECT_STREQ(CustomType::kToString, sb.data());
+  EXPECT_EQ(std::strlen(CustomType::kToString), sb.size());
+}
+
+TEST(MakeString, IntegerTypes) {
+  EXPECT_STREQ("0123-4567",
+               MakeString(0ll,
+                          1u,
+                          2l,
+                          3,
+                          -4,
+                          static_cast<unsigned short>(5),
+                          static_cast<short>(6),
+                          static_cast<unsigned char>(7))
+                   .data());
+}
+
+TEST(MakeString, Char) {
+  EXPECT_STREQ("a b c", MakeString('a', ' ', 'b', ' ', 'c').data());
+}
+
+TEST(MakeString, Float) { EXPECT_STREQ("-inf", MakeString(-INFINITY).data()); }
+
+TEST(MakeString, Pointer_Null) {
+  EXPECT_STREQ("(null)", MakeString(nullptr).data());
+  EXPECT_STREQ("(null)", MakeString(static_cast<void*>(nullptr)).data());
+}
+
+TEST(MakeString, Pointer_NonNull) {
+  EXPECT_STREQ("1", MakeString(reinterpret_cast<void*>(0x1)).data());
+  EXPECT_STREQ("123", MakeString(reinterpret_cast<int*>(0x123)).data());
+}
+
+TEST(MakeString, Pointer_CustomType) {
+  char expected[32] = {};
+
+  CustomType custom;
+  std::snprintf(expected,
+                sizeof(expected),
+                "%" PRIxPTR,
+                reinterpret_cast<uintptr_t>(&custom));
+
+  EXPECT_STREQ(expected, MakeString(&custom).data());
+}
+
+TEST(MakeString, Bool) {
+  EXPECT_STREQ("true", MakeString(true).data());
+  EXPECT_STREQ("false", MakeString(false).data());
+}
+
+TEST(MakeString, MutableString) {
+  char chars[] = {'C', 'o', 'o', 'l', '\0'};
+  EXPECT_STREQ("Cool?", MakeString(chars, "?").data());
+}
+
+TEST(MakeString, Empty_IsEmpty) { EXPECT_TRUE(MakeString().empty()); }
+
+constexpr char kLongestString[] = "18446744073709551615";  // largest uint64_t
+
+TEST(MakeString, DefaultSizeString_FitsWholeString) {
+  EXPECT_STREQ(
+      kLongestString,
+      MakeString(184, "467", u'\x04', "40", '7', '3', '7', "0", "", 955ul, 1615)
+          .data());
+}
+
+TEST(MakeString, LargerThanDefaultSize_Truncates) {
+  auto sb = MakeString("1844674407", 3709551615, 123456);
+
+  EXPECT_EQ(Status::RESOURCE_EXHAUSTED, sb.status());
+  EXPECT_STREQ(kLongestString, sb.data());
+}
+
+TEST(MakeString, StringLiteral_ResizesToFitWholeLiteral) {
+  EXPECT_STREQ("", MakeString().data());
+
+  auto normal = MakeString("");
+  static_assert(normal.max_size() == decltype(MakeString(1))::max_size());
+
+  auto resized = MakeString("This string is reeeeeeeeeaaaaallly long!!!!!");
+  static_assert(resized.max_size() > decltype(MakeString(1))::max_size());
+  static_assert(resized.max_size() ==
+                sizeof("This string is reeeeeeeeeaaaaallly long!!!!!") - 1);
+}
+
+TEST(MakeString, StringLiteral_UsesLongerFixedSize) {
+  auto fixed_size = MakeString<64>("");
+  static_assert(fixed_size.max_size() == 63u);
+  EXPECT_EQ(fixed_size.max_size(), 63u);
+  EXPECT_STREQ("", fixed_size.data());
+}
+
+TEST(MakeString, StringLiteral_TruncatesShorterFixedSize) {
+  EXPECT_STREQ("Goo", MakeString<4>("Google").data());
+  EXPECT_STREQ("Google", MakeString<7>("Google").data());
+  EXPECT_EQ(MakeString().max_size(), MakeString("Google").max_size());
+  EXPECT_STREQ("Google", MakeString("Google").data());
+}
+
+TEST(MakeString, DefaultSize_FitsMaxAndMinInts) {
+  EXPECT_STREQ("-9223372036854775808",
+               MakeString(std::numeric_limits<int64_t>::min()).data());
+  EXPECT_STREQ("18446744073709551615",
+               MakeString(std::numeric_limits<uint64_t>::max()).data());
+}
+
+TEST(MakeString, OutputToTemporaryStringBuffer) {
+  EXPECT_STREQ("hello", (MakeString<6>("hello ") << "world").data());
+  EXPECT_STREQ("hello world", (MakeString("hello ") << "world").data());
+}
+
+// Test MakeString's default size calculations.
+template <typename... Args>
+constexpr size_t DefaultStringBufferSize(Args&&...) {
+  return string_internal::DefaultStringBufferSize<Args...>();
+}
+
+// Default sizes are rounded up to 24 bytes.
+static_assert(DefaultStringBufferSize("") == 24);
+static_assert(DefaultStringBufferSize("123") == 24);
+static_assert(DefaultStringBufferSize("123", "456", "78901234567890") == 24);
+static_assert(DefaultStringBufferSize("1234567890", "1234567890", "123") == 24);
+static_assert(DefaultStringBufferSize(1234, 5678, 9012) == 24);
+
+// The buffer is sized to fix strings needing more than 24 bytes.
+static_assert(DefaultStringBufferSize("1234567890", "1234567890", "1234") ==
+              25);
+static_assert(DefaultStringBufferSize("1234567890", "1234567890", "12345") ==
+              26);
+static_assert(DefaultStringBufferSize("1234567890", "1234567890", "12345678") ==
+              29);
+
+// Four bytes are allocated for each non-string argument.
+static_assert(DefaultStringBufferSize(1234, 5678, 9012, 3456, 7890, 1234) ==
+              25);
+static_assert(DefaultStringBufferSize('a', nullptr, 'b', 4, 5, 6, 7, 8) == 33);
+
+}  // namespace
+}  // namespace pw
diff --git a/pw_string/to_string_test.cc b/pw_string/to_string_test.cc
index 125cda0..437d134 100644
--- a/pw_string/to_string_test.cc
+++ b/pw_string/to_string_test.cc
@@ -22,6 +22,7 @@
 
 #include "gtest/gtest.h"
 #include "pw_status/status.h"
+#include "pw_string/type_to_string.h"
 
 namespace pw {
 
@@ -117,8 +118,6 @@
   EXPECT_STREQ("-NaN", buffer);
 }
 
-constexpr std::string_view kNullString = "(null)";
-
 TEST(ToString, Pointer_NonNull_WritesValue) {
   CustomType custom;
   const size_t length = std::snprintf(expected,
@@ -135,24 +134,25 @@
 }
 
 TEST(ToString, Pointer_Nullptr_WritesNull) {
-  EXPECT_EQ(kNullString.size(), ToString(nullptr, buffer).size());
-  EXPECT_EQ(kNullString, buffer);
+  EXPECT_EQ(string::kNullPointerString.size(),
+            ToString(nullptr, buffer).size());
+  EXPECT_EQ(string::kNullPointerString, buffer);
 }
 
 TEST(ToString, Pointer_NullValuedPointer_WritesNull) {
-  EXPECT_EQ(kNullString.size(),
+  EXPECT_EQ(string::kNullPointerString.size(),
             ToString(static_cast<const CustomType*>(nullptr), buffer).size());
-  EXPECT_EQ(kNullString, buffer);
+  EXPECT_EQ(string::kNullPointerString, buffer);
 }
 
 TEST(ToString, Pointer_NullValuedCString_WritesNull) {
-  EXPECT_EQ(kNullString.size(),
+  EXPECT_EQ(string::kNullPointerString.size(),
             ToString(static_cast<char*>(nullptr), buffer).size());
-  EXPECT_EQ(kNullString, buffer);
+  EXPECT_EQ(string::kNullPointerString, buffer);
 
-  EXPECT_EQ(kNullString.size(),
+  EXPECT_EQ(string::kNullPointerString.size(),
             ToString(static_cast<const char*>(nullptr), buffer).size());
-  EXPECT_EQ(kNullString, buffer);
+  EXPECT_EQ(string::kNullPointerString, buffer);
 }
 
 TEST(ToString, String_Literal) {
@@ -218,9 +218,9 @@
   EXPECT_STREQ("false", test_buffer.data());
   EXPECT_EQ(2u, ToString("Hi", test_buffer).size());
   EXPECT_STREQ("Hi", test_buffer.data());
-  EXPECT_EQ(kNullString.size(),
+  EXPECT_EQ(string::kNullPointerString.size(),
             ToString(static_cast<void*>(nullptr), test_buffer).size());
-  EXPECT_EQ(kNullString, test_buffer.data());
+  EXPECT_EQ(string::kNullPointerString, test_buffer.data());
 }
 
 TEST(ToString, StringView) {
diff --git a/pw_string/type_to_string.cc b/pw_string/type_to_string.cc
index d317567..8255e28 100644
--- a/pw_string/type_to_string.cc
+++ b/pw_string/type_to_string.cc
@@ -172,10 +172,8 @@
 }
 
 StatusWithSize PointerToString(const void* pointer, const span<char>& buffer) {
-  static constexpr std::string_view kNullString("(null)");
-
   if (pointer == nullptr) {
-    return CopyEntireString(kNullString, buffer);
+    return CopyEntireString(kNullPointerString, buffer);
   }
   return IntToHexString(reinterpret_cast<uintptr_t>(pointer), buffer);
 }
diff --git a/pw_string/util_test.cc b/pw_string/util_test.cc
new file mode 100644
index 0000000..25375b9
--- /dev/null
+++ b/pw_string/util_test.cc
@@ -0,0 +1,41 @@
+// Copyright 2019 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_string/util.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::string {
+namespace {
+
+TEST(Length, Nullptr_Returns0) { EXPECT_EQ(0u, Length(nullptr, 100)); }
+
+TEST(Length, EmptyString_Returns0) {
+  EXPECT_EQ(0u, Length("", 0));
+  EXPECT_EQ(0u, Length("", 100));
+}
+
+TEST(Length, MaxLongerThanString_ReturnsStrlen) {
+  EXPECT_EQ(5u, Length("12345", 100));
+}
+
+TEST(Length, StringMaxLongerThanMax_ReturnsMax) {
+  EXPECT_EQ(0u, Length("12345", 0));
+  EXPECT_EQ(4u, Length("12345", 4));
+}
+
+TEST(Length, LengthEqualsMax) { EXPECT_EQ(5u, Length("12345", 5)); }
+
+}  // namespace
+}  // namespace pw::string