Add FileReader flag to decide if errors in opening file fatal/non-fatal

Flag default is fatal and will crash if errors in opening. If non-fatal,
log error instead of crashing. Added tests for the same.

Change-Id: I788416b86628055f137d0fc4a18fb62c0ca9177a
Signed-off-by: James Kuszmaul <james.kuszmaul@bluerivertech.com>
diff --git a/aos/util/file.cc b/aos/util/file.cc
index b2528a9..f1afa0f 100644
--- a/aos/util/file.cc
+++ b/aos/util/file.cc
@@ -202,9 +202,14 @@
   return span;
 }
 
-FileReader::FileReader(std::string_view filename)
+FileReader::FileReader(std::string_view filename,
+                       FileReaderErrorType error_type)
     : file_(open(::std::string(filename).c_str(), O_RDONLY)) {
-  PCHECK(file_.get() != -1) << ": opening " << filename;
+  if (!is_open()) {
+    PLOG_IF(FATAL, error_type == FileReaderErrorType::kFatal)
+        << ": opening " << filename;
+    PLOG(ERROR) << "opening " << filename;
+  }
 }
 
 std::optional<absl::Span<char>> FileReader::ReadContents(
diff --git a/aos/util/file.h b/aos/util/file.h
index 2fc4b2b..d0037fd 100644
--- a/aos/util/file.h
+++ b/aos/util/file.h
@@ -52,6 +52,8 @@
 
 enum class FileOptions { kReadable, kWriteable };
 
+enum class FileReaderErrorType { kFatal, kNonFatal };
+
 // Maps file from disk into memory
 std::shared_ptr<absl::Span<uint8_t>> MMapFile(
     const std::string &path, FileOptions options = FileOptions::kReadable);
@@ -61,7 +63,10 @@
 // but where you still want to read a file.
 class FileReader {
  public:
-  FileReader(std::string_view filename);
+  // Opens the file for reading. Crashes if file does not open when flag is set
+  // to fatal (default). Logs error if file does not open and flag is non-fatal.
+  FileReader(std::string_view filename,
+             FileReaderErrorType error_type = FileReaderErrorType::kFatal);
   // Reads the entire contents of the file into the internal buffer and returns
   // a string_view of it.  Returns nullopt if we failed to read the contents
   // (currently only on EREMOTEIO). Note: The result may not be null-terminated.
@@ -89,6 +94,9 @@
   // must be base 10.  Returns nullopt if we failed to read the file.
   std::optional<int32_t> ReadInt32();
 
+  // Checks if file could be opened.
+  bool is_open() const { return file_.get() != -1; }
+
  private:
   aos::ScopedFD file_;
 };
diff --git a/aos/util/file_test.cc b/aos/util/file_test.cc
index bd319b5..fb1befc 100644
--- a/aos/util/file_test.cc
+++ b/aos/util/file_test.cc
@@ -87,6 +87,7 @@
   ASSERT_EQ(0, system(("echo 123456789 > " + test_file).c_str()));
 
   FileReader reader(test_file);
+  EXPECT_TRUE(reader.is_open());
 
   aos::ScopedRealtime realtime;
   {
@@ -105,6 +106,30 @@
   EXPECT_EQ(123456789, reader.ReadInt32());
 }
 
+// Test reading a non-existent file.
+TEST(FileDeathTest, ReadNonExistentFile) {
+  const ::std::string test_file = "/dne";
+
+  // If error_type flag is not set or set to kFatal, this should fail.
+  EXPECT_DEATH(FileReader reader(test_file),
+               "opening " + test_file + ": No such file or directory");
+
+  FileReaderErrorType error_type = FileReaderErrorType::kFatal;
+  EXPECT_DEATH(FileReader reader(test_file, error_type),
+               "opening " + test_file + ": No such file or directory");
+
+  // If warning flag is set to true, read should not fail, is_open() should
+  // return false, ReadContents() and ReadInt32() should fail.
+  error_type = FileReaderErrorType::kNonFatal;
+  FileReader reader(test_file, error_type);
+  EXPECT_FALSE(reader.is_open());
+  std::array<char, 16> contents;
+  EXPECT_DEATH(
+      reader.ReadContents(absl::Span<char>(contents.data(), contents.size())),
+      "Bad file descriptor");
+  EXPECT_DEATH(reader.ReadInt32(), "Bad file descriptor");
+}
+
 // Tests that we can write to a file without malloc'ing.
 TEST(FileTest, WriteNormalFileNoMalloc) {
   const ::std::string tmpdir(aos::testing::TestTmpDir());