diff options
author | Wyatt Hepler <hepler@google.com> | 2019-11-11 10:45:48 -0800 |
---|---|---|
committer | Wyatt Hepler <hepler@google.com> | 2019-11-25 18:08:31 +0000 |
commit | ce9b952f0578ec13fc658eabe035e143343671fe (patch) | |
tree | 3d3fd8404eb195fb1909a5b29be24083a335bcd0 /pw_string | |
parent | 204b00d56153175abfa853d09810248291c3254d (diff) | |
download | pigweed-ce9b952f0578ec13fc658eabe035e143343671fe.tar.gz |
pw_string: StringBuilder utility class
StringBuilder can be used to flexibly and safely build strings in
fixed-size buffers.
Change-Id: Ie4453e1bcd11ef522bb0211304e9cf758ac17827
Diffstat (limited to 'pw_string')
-rw-r--r-- | pw_string/BUILD | 11 | ||||
-rw-r--r-- | pw_string/BUILD.gn | 22 | ||||
-rw-r--r-- | pw_string/public/pw_string/string_builder.h | 422 | ||||
-rw-r--r-- | pw_string/public/pw_string/type_to_string.h | 6 | ||||
-rw-r--r-- | pw_string/public/pw_string/util.h | 38 | ||||
-rw-r--r-- | pw_string/string_builder.cc | 123 | ||||
-rw-r--r-- | pw_string/string_builder_test.cc | 569 | ||||
-rw-r--r-- | pw_string/to_string_test.cc | 24 | ||||
-rw-r--r-- | pw_string/type_to_string.cc | 4 | ||||
-rw-r--r-- | pw_string/util_test.cc | 41 |
10 files changed, 1244 insertions, 16 deletions
diff --git a/pw_string/BUILD b/pw_string/BUILD index 3351fdc4b..544a1de7d 100644 --- a/pw_string/BUILD +++ b/pw_string/BUILD @@ -26,12 +26,15 @@ pw_cc_library( 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( ) 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 3221e0214..1e5481be1 100644 --- a/pw_string/BUILD.gn +++ b/pw_string/BUILD.gn @@ -28,9 +28,11 @@ source_set("pw_string") { "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("format_test") { ] } +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("type_to_string_test") { ] } +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 000000000..cb735b7af --- /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 4ce31dffc..b9bc52ddc 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 @@ StatusWithSize FloatAsIntToString(float value, const span<char>& buffer); // 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 000000000..0375802fa --- /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 000000000..4765126c4 --- /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 000000000..b2701e88d --- /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 125cda0d3..437d1340c 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 @@ TEST(ToString, Float) { 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_NonNull_WritesValue) { } 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 @@ TEST(ToString, StdArrayAsBuffer) { 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 d3175672b..8255e2848 100644 --- a/pw_string/type_to_string.cc +++ b/pw_string/type_to_string.cc @@ -172,10 +172,8 @@ StatusWithSize BoolToString(bool value, const span<char>& buffer) { } 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 000000000..25375b956 --- /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 |