Add Status object to AOS
This provides a type, comparable to absl::Status or std::error_code,
that can be used for communicating error messages back to users.
Future revisions may introduce additional sophistication, with either
more elaborate error codes or with more ability to communicate
structured information at run-time. However, doing those things in ways
that play nice with realtime code will require some additional effort,
so for now we keep the API relatively simple.
Change-Id: I9b9fa89bfd37bb18dbac7672939a867dac81600a
Signed-off-by: James Kuszmaul <james.kuszmaul@bluerivertech.com>
diff --git a/aos/containers/inlined_vector.h b/aos/containers/inlined_vector.h
index 5cf33a4..a92e46b 100644
--- a/aos/containers/inlined_vector.h
+++ b/aos/containers/inlined_vector.h
@@ -10,11 +10,17 @@
namespace aos {
template <typename T, size_t N>
-struct InlinedVector : public absl::InlinedVector<T, N> {};
+struct InlinedVector : public absl::InlinedVector<T, N> {
+ public:
+ using absl::InlinedVector<T, N>::InlinedVector;
+};
// Specialized for the N == 0 case because absl::InlinedVector doesn't support
// it for some reason.
template <typename T>
-struct InlinedVector<T, 0> : public std::vector<T> {};
+struct InlinedVector<T, 0> : public std::vector<T> {
+ public:
+ using std::vector<T>::vector;
+};
} // namespace aos
#endif // AOS_CONTAINERS_INLINED_VECTOR_H_
diff --git a/aos/util/BUILD b/aos/util/BUILD
index fa73d5e..7e19d4d 100644
--- a/aos/util/BUILD
+++ b/aos/util/BUILD
@@ -608,3 +608,27 @@
"@com_google_absl//absl/strings",
],
)
+
+cc_library(
+ name = "status",
+ srcs = ["status.cc"],
+ hdrs = ["status.h"],
+ target_compatible_with = ["@platforms//os:linux"],
+ deps = [
+ "//aos/containers:inlined_vector",
+ "@com_github_google_glog//:glog",
+ "@com_github_tartanllama_expected",
+ "@com_google_absl//absl/strings:str_format",
+ ],
+)
+
+cc_test(
+ name = "status_test",
+ srcs = ["status_test.cc"],
+ deps = [
+ ":status",
+ "//aos:realtime",
+ "//aos/testing:googletest",
+ "//aos/testing:path",
+ ],
+)
diff --git a/aos/util/status.cc b/aos/util/status.cc
new file mode 100644
index 0000000..de372db
--- /dev/null
+++ b/aos/util/status.cc
@@ -0,0 +1,55 @@
+#include "aos/util/status.h"
+
+namespace aos {
+Status::Status(StatusCode code, std::string_view message,
+ std::optional<std::source_location> source_location)
+ : code_(code),
+ owned_message_(message.begin(), message.end()),
+ message_(owned_message_.data(), owned_message_.size()),
+ source_location_(std::move(source_location)) {}
+Status::Status(StatusCode code, const char *message,
+ std::optional<std::source_location> source_location)
+ : code_(code),
+ message_(message),
+ source_location_(std::move(source_location)) {}
+
+Status::Status(Status &&other)
+ : code_(other.code_),
+ owned_message_(std::move(other.owned_message_)),
+ message_(MakeStringViewFromBufferOrView(owned_message_, other.message_)),
+ source_location_(std::move(other.source_location_)) {
+ // Because the internal string view contains a pointer to the owned_message_
+ // buffer, we need to have a manually written move constructor to manage it.
+ other.message_ = {};
+}
+Status &Status::operator=(Status &&other) {
+ std::swap(*this, other);
+ return *this;
+}
+Status::Status(const Status &other)
+ : code_(other.code_),
+ owned_message_(other.owned_message_),
+ message_(MakeStringViewFromBufferOrView(owned_message_, other.message_)),
+ source_location_(other.source_location_) {}
+
+std::string Status::ToString() const {
+ std::string source_info = "";
+ if (source_location_.has_value()) {
+ source_info = absl::StrFormat(
+ "%s:%d in %s: ", source_location_->file_name(),
+ source_location_->line(), source_location_->function_name());
+ }
+
+ return absl::StrFormat("%sStatus is %s with code of %d and message: %s",
+ source_info, ok() ? "okay" : "errored", code(),
+ message());
+}
+
+template <>
+void CheckExpected<void>(const tl::expected<void, Status> &expected) {
+ if (expected.has_value()) {
+ return;
+ }
+ LOG(FATAL) << expected.error().ToString();
+}
+} // namespace aos
diff --git a/aos/util/status.h b/aos/util/status.h
new file mode 100644
index 0000000..314936b
--- /dev/null
+++ b/aos/util/status.h
@@ -0,0 +1,149 @@
+#ifndef AOS_UTIL_STATUS_H_
+#define AOS_UTIL_STATUS_H_
+#include <optional>
+#include <source_location>
+#include <string_view>
+
+#include "absl/strings/str_format.h"
+#include "glog/logging.h"
+#include "tl/expected.hpp"
+
+#include "aos/containers/inlined_vector.h"
+
+namespace aos {
+// The Status class provides a means by which errors can be readily returned
+// from methods. It will typically be wrapped by an std::expected<> to
+// accommodate a return value or the Status, although an "ok" status can also be
+// used to indicate no-error.
+//
+// The Status class is similar to the absl::Status or std::error_code classes,
+// in that it consists of an integer error code of some sort (where 0 indicates
+// "ok") and a string error message of some sort. The main additions of this
+// class are:
+// 1. Adding a first-class exposure of an std::source_location to make exposure
+// of the sources of errors easier.
+// 2. Providing an interface that allows for Status implementations that expose
+// messages without malloc'ing (not possible with absl::Status, although it
+// is possible with std::error_code).
+// 3. Making it relatively easy to quickly return a simple error & message
+// (specifying a custom error with std::error_code is possible but requires
+// jumping through hoops and managing some global state).
+//
+// The goal of this class is that it should be easy to convert from exiting
+// error types (absl::Status, std::error_code) to this type.
+class Status {
+ public:
+ // In order to allow simple error messages without memory allocation, we
+ // reserve a small amount of stack space for error messages. This constant
+ // specifies the length of these strings.
+ static constexpr size_t kStaticMessageLength = 128;
+ // Attaches human-readable status enums to integer codes---the specific
+ // numeric codes are used as exit codes when terminating execution of the
+ // program.
+ // Note: While 0 will always indicate success and non-zero values will always
+ // indicate failures we may attempt to further expand the set of non-zero exit
+ // codes in the future and may decide to reuse 1 for a more specific error
+ // code at the time (although it is reasonably likely that it will be kept as
+ // a catch-all general error).
+ enum class StatusCode : int {
+ kOk = 0,
+ kError = 1,
+ };
+ // Constructs a status that indicates success, with no associated error
+ // message our source location.
+ static Status Ok() { return Status(StatusCode::kOk, "", std::nullopt); }
+ // Constructs an Error, copying the provided message. If the message is
+ // shorter than kStaticMessageLength, then the message will be stored entirely
+ // on the stack; longer messages will require dynamic memory allocation.
+ // The default source_location will correspond to the call-site of the
+ // Status::Error() method. This should only be overridden by wrappers that
+ // want to present a fancier interface to users.
+ static Status Error(
+ std::string_view message,
+ std::source_location source_location = std::source_location::current()) {
+ return Status(StatusCode::kError, message, std::move(source_location));
+ }
+ static tl::unexpected<Status> UnexpectedError(
+ std::string_view message,
+ std::source_location source_location = std::source_location::current()) {
+ return tl::unexpected<Status>(Error(message, std::move(source_location)));
+ }
+ // Constructs an error, retaining the provided pointer to a null-terminated
+ // error message. It is assumed that the message pointer will stay valid
+ // ~indefinitely. This is generally only appropriate to use with string
+ // literals (e.g., Status::StringLiteralError("Hello, World!")).
+ // The default source_location will correspond to the call-site of the
+ // Status::Error() method. This should only be overridden by wrappers that
+ // want to present a fancier interface to users.
+ static Status StringLiteralError(
+ const char *message,
+ std::source_location source_location = std::source_location::current()) {
+ return Status(StatusCode::kError, message, std::move(source_location));
+ }
+ static tl::unexpected<Status> UnexpectedStringLiteralError(
+ const char *message,
+ std::source_location source_location = std::source_location::current()) {
+ return tl::unexpected<Status>(
+ StringLiteralError(message, std::move(source_location)));
+ }
+
+ Status(Status &&other);
+ Status &operator=(Status &&other);
+ Status(const Status &other);
+
+ // Returns true if the Status indicates success.
+ [[nodiscard]] bool ok() const { return code_ == StatusCode::kOk; }
+ // Returns a numeric value for the status code. Zero will always indicate
+ // success; non-zero values will always indicate an error.
+ [[nodiscard]] int code() const { return static_cast<int>(code_); }
+ // Returns a view of the error message.
+ [[nodiscard]] std::string_view message() const { return message_; }
+ // Returns the source_location attached to the current Status. If the
+ // source_location was never set, will return nullopt. The source_location
+ // will typically be left unset for successful ("ok") statuses.
+ [[nodiscard]] const std::optional<std::source_location> &source_location()
+ const {
+ return source_location_;
+ }
+
+ std::string ToString() const;
+
+ private:
+ Status(StatusCode code, std::string_view message,
+ std::optional<std::source_location> source_location);
+ Status(StatusCode code, const char *message,
+ std::optional<std::source_location> source_location);
+
+ // Constructs a string view from the provided buffer if it has data and
+ // otherwise uses the provided string view. Used in copy/move constructors to
+ // figure out whether we should use the buffer or keep the pointer to the
+ // existing std::string_view (as is the case for when we store a pointer to a
+ // string literal).
+ static std::string_view MakeStringViewFromBufferOrView(
+ const aos::InlinedVector<char, kStaticMessageLength> &buffer,
+ const std::string_view &view) {
+ return (buffer.size() > 0) ? std::string_view(buffer.begin(), buffer.end())
+ : view;
+ }
+
+ StatusCode code_;
+ aos::InlinedVector<char, kStaticMessageLength> owned_message_;
+ std::string_view message_;
+ std::optional<std::source_location> source_location_;
+};
+
+// Dies fatally if the provided expected does not include the value T, printing
+// out an error message that includes the Status on the way out.
+// Returns the stored value on success.
+template <typename T>
+T CheckExpected(const tl::expected<T, Status> &expected) {
+ if (expected.has_value()) {
+ return expected.value();
+ }
+ LOG(FATAL) << expected.error().ToString();
+}
+
+template <>
+void CheckExpected<void>(const tl::expected<void, Status> &expected);
+} // namespace aos
+#endif // AOS_UTIL_STATUS_H_
diff --git a/aos/util/status_test.cc b/aos/util/status_test.cc
new file mode 100644
index 0000000..0caf73d
--- /dev/null
+++ b/aos/util/status_test.cc
@@ -0,0 +1,116 @@
+#include "aos/util/status.h"
+
+#include <filesystem>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "aos/realtime.h"
+#include "aos/testing/path.h"
+
+DECLARE_bool(die_on_malloc);
+
+namespace aos::testing {
+class StatusTest : public ::testing::Test {
+ protected:
+ StatusTest() {}
+};
+
+// Tests that we can construct an "Ok" status and that it presents the correct
+// interface.
+TEST_F(StatusTest, Okay) {
+ std::optional<Status> ok;
+ {
+ aos::ScopedRealtime realtime;
+ ok = Status::Ok();
+ }
+ ASSERT_TRUE(ok.has_value());
+ EXPECT_TRUE(ok->ok());
+ EXPECT_EQ(0, ok->code());
+ EXPECT_EQ("", ok->message());
+ EXPECT_FALSE(ok->source_location().has_value());
+ EXPECT_EQ(std::string("Status is okay with code of 0 and message: "),
+ ok->ToString());
+}
+
+// Tests that we can construct an errored status in realtime code.
+TEST_F(StatusTest, RealtimeError) {
+ std::optional<Status> error;
+ {
+ aos::ScopedRealtime realtime;
+ error = Status::Error("Hello, World!");
+ }
+ const int line = __LINE__ - 2;
+ ASSERT_TRUE(error.has_value());
+ EXPECT_FALSE(error->ok());
+ EXPECT_NE(0, error->code());
+ EXPECT_EQ(std::string("Hello, World!"), error->message());
+ ASSERT_TRUE(error->source_location().has_value());
+ EXPECT_EQ(
+ std::string("status_test.cc"),
+ std::filesystem::path(error->source_location()->file_name()).filename());
+ EXPECT_EQ(
+ std::string("virtual void "
+ "aos::testing::StatusTest_RealtimeError_Test::TestBody()"),
+ error->source_location()->function_name());
+ EXPECT_EQ(line, error->source_location()->line());
+ EXPECT_LT(1, error->source_location()->column());
+ EXPECT_THAT(
+ error->ToString(),
+ ::testing::HasSubstr(absl::StrFormat(
+ "status_test.cc:%d in virtual void "
+ "aos::testing::StatusTest_RealtimeError_Test::TestBody(): Status is "
+ "errored with code of 1 and message: Hello, World!",
+ line)));
+}
+
+// Tests that we do indeed malloc (and catch it) on an extra-long error message
+// (this is mostly intended to ensure that the test setup is working correctly).
+TEST(StatusDeatTest, BlowsUpOnRealtimeAllocation) {
+ std::string message(" ", Status::kStaticMessageLength + 1);
+ EXPECT_DEATH(
+ {
+ aos::ScopedRealtime realtime;
+ aos::CheckRealtime();
+ Status foo = Status::Error(message);
+ },
+ "Malloced");
+}
+
+// Tests that we can use arbitrarily-sized string literals for error messages.
+TEST(StatusDeatTest, StringLiteralError) {
+ std::optional<Status> error;
+ const char *message =
+ "Hellllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll"
+ "llllllllllllllloooooooooooooooooooooooooooooooooooooooooooo, "
+ "World!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
+ "!!!!!!!!!!!!!!";
+ ASSERT_LT(Status::kStaticMessageLength, strlen(message));
+ {
+ aos::ScopedRealtime realtime;
+ error = Status::StringLiteralError(message);
+ }
+ ASSERT_TRUE(error.has_value());
+ EXPECT_FALSE(error->ok());
+ EXPECT_EQ(message, error->message());
+ ASSERT_TRUE(error->source_location().has_value());
+ EXPECT_EQ(
+ std::string("status_test.cc"),
+ std::filesystem::path(error->source_location()->file_name()).filename());
+}
+
+// Tests that the CheckExpected() call works as intended.
+TEST(StatusDeathTest, CheckExpected) {
+ tl::expected<int, Status> expected;
+ expected.emplace(971);
+ EXPECT_EQ(971, CheckExpected(expected))
+ << "Should have gotten out the emplaced value on no error.";
+ expected = Status::UnexpectedError("Hello, World!");
+ EXPECT_DEATH(CheckExpected(expected), "Hello, World!")
+ << "An error message including the error string should have been printed "
+ "on death.";
+ EXPECT_DEATH(CheckExpected<void>(Status::UnexpectedError("void expected")),
+ "void expected")
+ << "A void expected should work with CheckExpected().";
+}
+} // namespace aos::testing