Add support for log-handler
This handler is based off of version handler with major differences in
handler_builder
Tested:
created the blackbox blob successfully using this lib.
Read the data from the blob using a host side tool
Signed-off-by: Gaurav Gandhi <gauravgandhi@google.com>
Change-Id: I9ef775af752156a1647453ff3831ef4c0449d546
diff --git a/bmc/log-handler/log_handler.cpp b/bmc/log-handler/log_handler.cpp
new file mode 100644
index 0000000..1883128
--- /dev/null
+++ b/bmc/log-handler/log_handler.cpp
@@ -0,0 +1,227 @@
+// Copyright 2021 Google Inc.
+//
+// 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
+//
+// http://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 "log_handler.hpp"
+
+#include <algorithm>
+#include <cstring>
+#include <ios>
+#include <limits>
+#include <memory>
+#include <optional>
+#include <utility>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+LogBlobHandler::LogBlobHandler(std::vector<HandlerConfig<ActionPack>>&& configs)
+{
+ for (auto& config : configs)
+ {
+ auto info = std::make_unique<BlobInfo>();
+ info->blobId = std::move(config.blobId);
+ info->actions = std::move(config.actions);
+ info->handler = std::move(config.handler);
+ info->actions->onOpen->setCallback(
+ [infoP = info.get()](TriggerableActionInterface& tai) {
+ auto data =
+ std::make_shared<std::optional<std::vector<uint8_t>>>();
+ do
+ {
+ if (tai.status() != ActionStatus::success)
+ {
+ fprintf(stderr,
+ "LogBlobHandler: Log file unit failed for %s\n",
+ infoP->blobId.c_str());
+ continue;
+ }
+ if (!infoP->handler->open("", std::ios::in))
+ {
+ fprintf(
+ stderr,
+ "LogBlobHandler: Opening log file failed for %s\n",
+ infoP->blobId.c_str());
+ continue;
+ }
+ auto d = infoP->handler->read(
+ 0, std::numeric_limits<uint32_t>::max());
+ infoP->handler->close();
+ if (!d)
+ {
+ fprintf(
+ stderr,
+ "LogBlobHandler: Reading log file failed for %s\n",
+ infoP->blobId.c_str());
+ continue;
+ }
+ *data = std::move(d);
+ } while (false);
+ for (auto sessionP : infoP->sessionsToUpdate)
+ {
+ sessionP->data = data;
+ }
+ infoP->sessionsToUpdate.clear();
+ });
+ if (!blobInfoMap.try_emplace(info->blobId, std::move(info)).second)
+ {
+ fprintf(stderr,
+ "LogBlobHandler: Ignoring duplicate config for %s\n",
+ info->blobId.c_str());
+ }
+ }
+}
+
+bool LogBlobHandler::canHandleBlob(const std::string& path)
+{
+ return blobInfoMap.find(path) != blobInfoMap.end();
+}
+
+std::vector<std::string> LogBlobHandler::getBlobIds()
+{
+ std::vector<std::string> ret;
+ for (const auto& [key, _] : blobInfoMap)
+ {
+ ret.emplace_back(key);
+ }
+ return ret;
+}
+
+/**
+ * deleteBlob - does nothing, always fails
+ */
+bool LogBlobHandler::deleteBlob(const std::string& path)
+{
+ for (const auto& [sessionId, sessionInfo] : sessionInfoMap)
+ {
+ if (sessionInfo->blob->blobId == path)
+ {
+ fprintf(stderr,
+ "LogBlobHandler: delete %s fail: there is an open session "
+ "for this blob\n",
+ path.c_str());
+ return false;
+ }
+ }
+
+ auto* blob = blobInfoMap.at(path).get();
+ if (!blob->actions->onDelete->trigger())
+ {
+ fprintf(stderr,
+ "LogBlobHandler: delete %s fail: onDelete trigger failed\n",
+ path.c_str());
+ return false;
+ }
+ return true;
+}
+
+bool LogBlobHandler::stat(const std::string&, blobs::BlobMeta*)
+{
+ return false;
+}
+
+bool LogBlobHandler::open(uint16_t session, uint16_t flags,
+ const std::string& path)
+{
+ /* only reads are supported, check if blob is handled and make sure
+ * the blob isn't already opened
+ */
+ if (flags != blobs::read)
+ {
+ fprintf(stderr,
+ "LogBlobHandler: open %s fail: unsupported flags(0x%04X.)\n",
+ path.c_str(), flags);
+ return false;
+ }
+
+ auto info = std::make_unique<SessionInfo>();
+ info->blob = blobInfoMap.at(path).get();
+ info->blob->sessionsToUpdate.emplace(info.get());
+ if (info->blob->sessionsToUpdate.size() == 1 &&
+ !info->blob->actions->onOpen->trigger())
+ {
+ fprintf(stderr, "LogBlobHandler: open %s fail: onOpen trigger failed\n",
+ path.c_str());
+ info->blob->sessionsToUpdate.erase(info.get());
+ return false;
+ }
+
+ sessionInfoMap[session] = std::move(info);
+ return true;
+}
+
+std::vector<uint8_t> LogBlobHandler::read(uint16_t session, uint32_t offset,
+ uint32_t requestedSize)
+{
+ auto& data = sessionInfoMap.at(session)->data;
+ if (data == nullptr || !*data)
+ {
+ throw std::runtime_error("LogBlobHandler: Log data not ready for read");
+ }
+ if ((*data)->size() < offset)
+ {
+ return {};
+ }
+ std::vector<uint8_t> ret(
+ std::min<size_t>(requestedSize, (*data)->size() - offset));
+ std::memcpy(&ret[0], &(**data)[offset], ret.size());
+ return ret;
+}
+
+bool LogBlobHandler::close(uint16_t session)
+{
+ auto it = sessionInfoMap.find(session);
+ if (it == sessionInfoMap.end())
+ {
+ return false;
+ }
+ auto& info = *it->second;
+ info.blob->sessionsToUpdate.erase(&info);
+ if (info.blob->sessionsToUpdate.empty())
+ {
+ info.blob->actions->onOpen->abort();
+ }
+ sessionInfoMap.erase(it);
+ return true;
+}
+
+bool LogBlobHandler::stat(uint16_t session, blobs::BlobMeta* meta)
+{
+ const auto& data = sessionInfoMap.at(session)->data;
+ if (data == nullptr)
+ {
+ meta->blobState = blobs::StateFlags::committing;
+ meta->size = 0;
+ }
+ else if (!*data)
+ {
+ meta->blobState = blobs::StateFlags::commit_error;
+ meta->size = 0;
+ }
+ else
+ {
+ meta->blobState =
+ blobs::StateFlags::committed | blobs::StateFlags::open_read;
+ meta->size = (*data)->size();
+ }
+ return true;
+}
+
+bool LogBlobHandler::expire(uint16_t session)
+{
+ close(session);
+ return true;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/log_handler.hpp b/bmc/log-handler/log_handler.hpp
new file mode 100644
index 0000000..32aebaa
--- /dev/null
+++ b/bmc/log-handler/log_handler.hpp
@@ -0,0 +1,110 @@
+// Copyright 2021 Google Inc.
+//
+// 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
+//
+// http://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 "handler_config.hpp"
+#include "image_handler.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <optional>
+#include <set>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+class LogBlobHandler : public blobs::GenericBlobInterface
+{
+ public:
+ struct ActionPack
+ {
+ /** Only file operation action supported currently */
+ std::unique_ptr<TriggerableActionInterface> onOpen;
+ std::unique_ptr<TriggerableActionInterface> onDelete;
+ };
+
+ /**
+ * Create a LogBlobHandler.
+ *
+ * @param[in] configs - list of blob configurations to support
+ */
+ LogBlobHandler(std::vector<HandlerConfig<ActionPack>>&& configs);
+
+ ~LogBlobHandler() = default;
+ LogBlobHandler(const LogBlobHandler&) = delete;
+ LogBlobHandler& operator=(const LogBlobHandler&) = delete;
+ LogBlobHandler(LogBlobHandler&&) = default;
+ LogBlobHandler& operator=(LogBlobHandler&&) = default;
+
+ bool canHandleBlob(const std::string& path) override;
+ std::vector<std::string> getBlobIds() override;
+ bool deleteBlob(const std::string& path) override;
+ bool stat(const std::string&, blobs::BlobMeta* meta) override;
+ bool open(uint16_t session, uint16_t flags,
+ const std::string& path) override;
+ std::vector<uint8_t> read(uint16_t session, uint32_t offset,
+ uint32_t requestedSize) override;
+ bool write(uint16_t, uint32_t, const std::vector<uint8_t>&) override
+ {
+ return false; /* not supported */
+ };
+ bool writeMeta(uint16_t, uint32_t, const std::vector<uint8_t>&) override
+ {
+ return false; /* not supported */
+ }
+ bool commit(uint16_t, const std::vector<uint8_t>&) override
+ {
+ return false; // not supported
+ }
+ bool close(uint16_t session) override;
+ bool stat(uint16_t session, blobs::BlobMeta* meta) override;
+ bool expire(uint16_t session) override;
+
+ private:
+ struct SessionInfo;
+
+ struct BlobInfo
+ {
+ Pinned<std::string> blobId;
+ std::unique_ptr<ActionPack> actions;
+ std::unique_ptr<ImageHandlerInterface> handler;
+ std::set<SessionInfo*> sessionsToUpdate;
+ };
+
+ struct SessionInfo
+ {
+ BlobInfo* blob;
+
+ // A cached copy of the version data shared by all clients for a single
+ // execution of the version retrieval action. This is is null until the
+ // TriggerableAction has completed. If the action is an error, the
+ // shared object is nullopt. Otherwise, contains a vector of the version
+ // data when successfully read.
+ std::shared_ptr<const std::optional<std::vector<uint8_t>>> data;
+ };
+
+ std::unordered_map<std::string_view, std::unique_ptr<BlobInfo>> blobInfoMap;
+ std::unordered_map<uint16_t, std::unique_ptr<SessionInfo>> sessionInfoMap;
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/log_handlers_builder.cpp b/bmc/log-handler/log_handlers_builder.cpp
new file mode 100644
index 0000000..2781534
--- /dev/null
+++ b/bmc/log-handler/log_handlers_builder.cpp
@@ -0,0 +1,125 @@
+// Copyright 2021 Google Inc.
+//
+// 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
+//
+// http://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 "log_handlers_builder.hpp"
+
+#include "file_handler.hpp"
+#include "fs.hpp"
+#include "skip_action.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <algorithm>
+#include <cstdio>
+#include <exception>
+#include <fstream>
+#include <memory>
+#include <regex>
+#include <string>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+std::vector<HandlerConfig<LogBlobHandler::ActionPack>>
+ LogHandlersBuilder::buildHandlerFromJson(const nlohmann::json& data)
+{
+ std::vector<HandlerConfig<LogBlobHandler::ActionPack>> handlers;
+
+ for (const auto& item : data)
+ {
+ try
+ {
+ HandlerConfig<LogBlobHandler::ActionPack> output;
+
+ /* at() throws an exception when the key is not present. */
+ item.at("blob").get_to(output.blobId);
+
+ /* name must be: /log/ */
+ std::regex regexpr("^\\/(?:log)\\/(.+)");
+ std::smatch matches;
+ if (!std::regex_search(output.blobId, matches, regexpr))
+ {
+ continue;
+ }
+ /* log must have handler */
+ const auto& h = item.at("handler");
+
+ const std::string& handlerType = h.at("type");
+ if (handlerType == "file")
+ {
+ const auto& path = h.at("path");
+ output.handler = std::make_unique<FileHandler>(path);
+ }
+ else
+ {
+ throw std::runtime_error("Invalid handler type: " +
+ handlerType);
+ }
+
+ /* actions are required (presently). */
+ const auto& a = item.at("actions");
+ auto pack = std::make_unique<LogBlobHandler::ActionPack>();
+
+ /* to make an action optional, assign type "skip" */
+ const auto& onOpen = a.at("open");
+ const std::string& onOpenType = onOpen.at("type");
+ if (onOpenType == "systemd")
+ {
+ pack->onOpen = std::move(buildSystemd(onOpen));
+ }
+ else if (onOpenType == "skip")
+ {
+ pack->onOpen = SkipAction::CreateSkipAction();
+ }
+ else
+ {
+ throw std::runtime_error("Invalid preparation type: " +
+ onOpenType);
+ }
+
+ const auto& onDelete = a.at("delete");
+ const std::string& onDeleteType = onOpen.at("type");
+ if (onOpenType == "systemd")
+ {
+ pack->onDelete = std::move(buildSystemd(onDelete));
+ }
+ else if (onOpenType == "skip")
+ {
+ pack->onDelete = SkipAction::CreateSkipAction();
+ }
+ else
+ {
+ throw std::runtime_error("Invalid preparation type: " +
+ onDeleteType);
+ }
+
+ output.actions = std::move(pack);
+ handlers.push_back(std::move(output));
+ }
+ catch (const std::exception& e)
+ {
+ /* TODO: Once phosphor-logging supports unit-test injection, fix
+ * this to log.
+ */
+ std::fprintf(stderr,
+ "Excepted building HandlerConfig from json: %s\n",
+ e.what());
+ }
+ }
+
+ return handlers;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/log_handlers_builder.hpp b/bmc/log-handler/log_handlers_builder.hpp
new file mode 100644
index 0000000..552758d
--- /dev/null
+++ b/bmc/log-handler/log_handlers_builder.hpp
@@ -0,0 +1,35 @@
+// Copyright 2021 Google Inc.
+//
+// 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
+//
+// http://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 "buildjson.hpp"
+#include "log_handler.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <vector>
+
+namespace ipmi_flash
+{
+/**
+ * provide the method to parse and validate blob entries from json and produce
+ * something that is usable by the log handler.
+ */
+class LogHandlersBuilder : public HandlersBuilderIfc<LogBlobHandler::ActionPack>
+{
+ public:
+ std::vector<HandlerConfig<LogBlobHandler::ActionPack>>
+ buildHandlerFromJson(const nlohmann::json& data) override;
+};
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/main.cpp b/bmc/log-handler/main.cpp
new file mode 100644
index 0000000..d860ca7
--- /dev/null
+++ b/bmc/log-handler/main.cpp
@@ -0,0 +1,24 @@
+// Copyright 2021 Google Inc.
+//
+// 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
+//
+// http://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 "log_handler.hpp"
+#include "log_handlers_builder.hpp"
+
+#include <memory>
+
+extern "C" std::unique_ptr<blobs::GenericBlobInterface> createHandler()
+{
+ return std::make_unique<ipmi_flash::LogBlobHandler>(
+ ipmi_flash::LogHandlersBuilder().buildHandlerConfigsFromDefaultPaths());
+}
diff --git a/bmc/log-handler/meson.build b/bmc/log-handler/meson.build
new file mode 100644
index 0000000..50c4bed
--- /dev/null
+++ b/bmc/log-handler/meson.build
@@ -0,0 +1,35 @@
+log_inc = include_directories('.')
+
+log_pre = declare_dependency(
+ include_directories: [root_inc, log_inc],
+ dependencies : [
+ common_dep,
+ firmware_dep,
+ ])
+
+log_lib = static_library(
+ 'logblob',
+ 'log_handler.cpp',
+ 'log_handlers_builder.cpp',
+ implicit_include_directories: false,
+ dependencies: log_pre)
+
+
+log_dep = declare_dependency(
+ link_with: log_lib,
+ dependencies: common_pre)
+
+shared_module(
+ 'logblob',
+ 'main.cpp',
+ implicit_include_directories: false,
+ dependencies: [
+ log_dep,
+ dependency('libipmid'),
+ ],
+ install: true,
+ install_dir: get_option('libdir') / 'blob-ipmid')
+
+if not get_option('tests').disabled()
+ subdir('test')
+endif
\ No newline at end of file
diff --git a/bmc/log-handler/test/log_canhandle_enumerate_unittest.cpp b/bmc/log-handler/test/log_canhandle_enumerate_unittest.cpp
new file mode 100644
index 0000000..f27737d
--- /dev/null
+++ b/bmc/log-handler/test/log_canhandle_enumerate_unittest.cpp
@@ -0,0 +1,43 @@
+// Copyright 2021 Google Inc.
+//
+// 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
+//
+// http://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 "log_handler.hpp"
+#include "log_mock.hpp"
+
+#include <array>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+
+TEST(LogHandlerCanHandleTest, VerifyGoodInfoMap)
+{
+ constexpr std::array blobNames{"blob0", "blob1", "blob2", "blob3"};
+ LogBlobHandler handler(createMockLogConfigs(blobNames));
+ for (const auto& blobName : blobNames)
+ {
+ EXPECT_TRUE(handler.canHandleBlob(blobName));
+ }
+}
+
+TEST(LogHandlerEnumerateTest, VerifyGoodInfoMap)
+{
+ constexpr std::array blobNames{"blob0", "blob1", "blob2", "blob3"};
+ LogBlobHandler handler(createMockLogConfigs(blobNames));
+ EXPECT_THAT(handler.getBlobIds(),
+ ::testing::UnorderedElementsAreArray(blobNames));
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_close_unittest.cpp b/bmc/log-handler/test/log_close_unittest.cpp
new file mode 100644
index 0000000..8755541
--- /dev/null
+++ b/bmc/log-handler/test/log_close_unittest.cpp
@@ -0,0 +1,94 @@
+// Copyright 2021 Google Inc.
+//
+// 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
+//
+// http://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 "log_handler.hpp"
+#include "log_mock.hpp"
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::Return;
+
+namespace ipmi_flash
+{
+
+class LogCloseExpireBlobTest : public ::testing::Test
+{
+ protected:
+ void SetUp() override
+ {
+ h = std::make_unique<LogBlobHandler>(
+ createMockLogConfigs(blobNames, &im, &tm));
+ }
+
+ std::unique_ptr<blobs::GenericBlobInterface> h;
+ std::vector<std::string> blobNames{"blob0", "blob1", "blob2", "blob3"};
+ std::unordered_map<std::string, TriggerMock*> tm;
+ std::unordered_map<std::string, ImageHandlerMock*> im;
+};
+
+TEST_F(LogCloseExpireBlobTest, VerifyOpenThenClose)
+{
+ EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+ EXPECT_CALL(*tm.at("blob0"), abort()).Times(1);
+ EXPECT_TRUE(h->close(0));
+}
+
+TEST_F(LogCloseExpireBlobTest, VerifySingleAbort)
+{
+ EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+ EXPECT_TRUE(h->open(1, blobs::read, "blob0"));
+ EXPECT_TRUE(h->close(0));
+ EXPECT_CALL(*tm.at("blob0"), abort()).Times(1);
+ EXPECT_TRUE(h->close(1));
+}
+
+TEST_F(LogCloseExpireBlobTest, VerifyUnopenedBlobCloseFails)
+{
+ EXPECT_FALSE(h->close(0));
+}
+
+TEST_F(LogCloseExpireBlobTest, VerifyDoubleCloseFails)
+{
+ EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+ EXPECT_CALL(*tm.at("blob0"), abort()).Times(1);
+ EXPECT_TRUE(h->close(0));
+ EXPECT_FALSE(h->close(0));
+}
+
+TEST_F(LogCloseExpireBlobTest, VerifyBadSessionNumberCloseFails)
+{
+ EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+ EXPECT_FALSE(h->close(1));
+ EXPECT_CALL(*tm.at("blob0"), abort()).Times(1);
+ EXPECT_TRUE(h->close(0));
+}
+
+TEST_F(LogCloseExpireBlobTest, VerifyRunningActionIsAborted)
+{
+ EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+ EXPECT_CALL(*tm.at("blob0"), abort()).Times(1);
+ EXPECT_TRUE(h->close(0));
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_createhandler_unittest.cpp b/bmc/log-handler/test/log_createhandler_unittest.cpp
new file mode 100644
index 0000000..99d5d3e
--- /dev/null
+++ b/bmc/log-handler/test/log_createhandler_unittest.cpp
@@ -0,0 +1,44 @@
+// Copyright 2021 Google Inc.
+//
+// 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
+//
+// http://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 "log_handler.hpp"
+#include "log_mock.hpp"
+
+#include <array>
+#include <utility>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+
+TEST(LogHandlerCanHandleTest, VerifyGoodInfoMapPasses)
+{
+ constexpr std::array blobs{"blob0", "blob1"};
+ LogBlobHandler handler(createMockLogConfigs(blobs));
+ EXPECT_THAT(handler.getBlobIds(),
+ testing::UnorderedElementsAreArray(blobs));
+}
+
+TEST(LogHandlerCanHandleTest, VerifyDuplicatesIgnored)
+{
+ constexpr std::array blobs{"blob0"};
+ auto configs = createMockLogConfigs(blobs);
+ configs.push_back(createMockLogConfig(blobs[0]));
+ LogBlobHandler handler(std::move(configs));
+ EXPECT_THAT(handler.getBlobIds(),
+ testing::UnorderedElementsAreArray(blobs));
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_json_unittest.cpp b/bmc/log-handler/test/log_json_unittest.cpp
new file mode 100644
index 0000000..5759c25
--- /dev/null
+++ b/bmc/log-handler/test/log_json_unittest.cpp
@@ -0,0 +1,314 @@
+// Copyright 2021 Google Inc.
+//
+// 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
+//
+// http://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 "general_systemd.hpp"
+#include "log_handlers_builder.hpp"
+#include "skip_action.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+using ::testing::IsEmpty;
+
+using json = nlohmann::json;
+
+TEST(LogJsonTest, ValidConfigurationNoLogHandler)
+{
+ auto j2 = R"(
+ [{
+ "blob" : "/flash/sink_seq",
+ "log":{
+ "handler": {
+ "type" : "file",
+ "path" : "/tmp/log_info"
+ },
+ "actions":{
+ "open" :{
+ "type" : "systemd",
+ "unit" : "absolute"
+ }
+ }
+ }
+ }]
+ )"_json;
+ auto h = LogHandlersBuilder().buildHandlerFromJson(j2);
+ ASSERT_THAT(h, ::testing::SizeIs(1));
+ EXPECT_THAT(h[0].blobId, "/log/sink_seq");
+ EXPECT_FALSE(h[0].actions == nullptr);
+ EXPECT_FALSE(h[0].handler == nullptr);
+}
+
+TEST(LogJsonTest, ValidConfigurationLogBlobName)
+{
+ auto j2 = R"(
+ [{
+ "blob" : "/log/sink_seq",
+ "log":{
+ "handler": {
+ "type" : "file",
+ "path" : "/tmp/log_info"
+ },
+ "actions": {
+ "open" : {
+ "type" : "systemd",
+ "unit" : "phosphor-ipmi-flash-log-sink-sequencer.target"
+ }
+ }
+ }
+ }]
+ )"_json;
+ auto h = LogHandlersBuilder().buildHandlerFromJson(j2);
+ ASSERT_THAT(h, ::testing::SizeIs(1));
+ EXPECT_THAT(h[0].blobId, "/log/sink_seq");
+ EXPECT_FALSE(h[0].actions == nullptr);
+ EXPECT_FALSE(h[0].handler == nullptr);
+}
+
+TEST(LogJsonTest, MissingHandlerType)
+{
+ auto j2 = R"(
+ [{
+ "blob" : "/flash/image",
+ "log":{
+ "handler": {
+ "path" : "/tmp/log_info"
+ },
+ "actions": {
+ "open" : {
+ "type" : "systemd",
+ "unit" : "absolute"}
+ }
+ }
+ }]
+ )"_json;
+ EXPECT_THAT(LogHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(LogJsonTest, BadBlobName)
+{
+ auto j2 = R"(
+ [{
+ "blob" : "/bad/image",
+ "log":{
+ "handler": {
+ "type" : "file",
+ "path" : "/tmp/log_info"
+ },
+ "actions": {
+ "open" : {
+ "type" : "systemd",
+ "unit" : "absolute"}
+ }
+ }
+ }]
+ )"_json;
+ EXPECT_THAT(LogHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(LogJsonTest, MissingActions)
+{
+ auto j2 = R"(
+ [{
+ "blob" : "/flash/image",
+ "log":{
+ "handler": {
+ "type" : "file",
+ "path" : "/tmp/log_info"
+ }
+ }
+ }]
+ )"_json;
+ EXPECT_THAT(LogHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(LogJsonTest, MissingOpenAction)
+{
+ auto j2 = R"(
+ [{
+ "blob" : "/flash/image",
+ "log":{
+ "handler": {
+ "type" : "file",
+ "path" : "/tmp/log_info"
+ },
+ "actions": {}
+ }
+ }]
+ )"_json;
+ EXPECT_THAT(LogHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(LogJsonTest, OneInvalidTwoValidSucceeds)
+{
+ auto j2 = R"(
+ [{
+ "blob" : "/flash/sink_seq0",
+ "log":{
+ "handler": {
+ "type" : "file",
+ "path" : "/tmp/log_info"
+ },
+ "actions":{
+ "open" :{
+ "type" : "systemd",
+ "unit" : "absolute"
+ }
+ }
+ }
+ },
+ {
+ "blob" : "/log/sink_seq1",
+ "log":{
+ "handler": {
+ "type" : "file",
+ "path" : "/tmp/log_info"
+ },
+ "actions":{
+ "open" :{
+ "type" : "systemd",
+ "unit" : "absolute"
+ }
+ }
+ }
+ },
+ {
+ "blob" : "/bad/sink_seq",
+ "log":{
+ "handler": {
+ "type" : "file",
+ "path" : "/tmp/log_info"
+ },
+ "actions":{
+ "open" :{
+ "type" : "systemd",
+ "unit" : "absolute"
+ }
+ }
+ }
+ }
+ ]
+ )"_json;
+ auto h = LogHandlersBuilder().buildHandlerFromJson(j2);
+ ASSERT_THAT(h, ::testing::SizeIs(2));
+ EXPECT_THAT(h[0].blobId, "/log/sink_seq0");
+ EXPECT_THAT(h[1].blobId, "/log/sink_seq1");
+}
+
+TEST(LogJsonTest, BlobNameIsTooShort)
+{
+ auto j2 = R"(
+ [{
+ "blob" : "/flash/",
+ "log":{
+ "handler": {
+ "type" : "file",
+ "path" : "/tmp/log_info"
+ },
+ "actions":{
+ "open" :{
+ "type" : "systemd",
+ "unit" : "absolute"
+ }
+ }
+ }
+ }]
+ )"_json;
+ EXPECT_THAT(LogHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(LogJsonTest, OpenSkipAction)
+{
+ auto j2 = R"(
+ [{
+ "blob" : "/flash/sink_seqs",
+ "log":{
+ "handler": {
+ "type" : "file",
+ "path" : "/tmp/log_info"
+ },
+ "actions":{
+ "open" :{
+ "type" : "skip"
+ }
+ }
+ }
+ }]
+ )"_json;
+ auto h = LogHandlersBuilder().buildHandlerFromJson(j2);
+ EXPECT_THAT(h, ::testing::SizeIs(1));
+ EXPECT_TRUE(h[0].blobId == "/log/sink_seqs");
+ ASSERT_FALSE(h[0].actions == nullptr);
+ EXPECT_FALSE(h[0].actions->onOpen == nullptr);
+}
+
+TEST(LogJsonTest, OpenActionsWithDifferentModes)
+{
+ auto j2 = R"(
+ [{
+ "blob" : "/flash/blob1",
+ "log":{
+ "handler": {
+ "type" : "file",
+ "path" : "/tmp/log_info"
+ },
+ "actions":{
+ "open" :{
+ "type" : "systemd",
+ "unit" : "absolute",
+ "mode" : "replace-nope"
+ }
+ }
+ }
+ },
+ {
+ "blob" : "/flash/blob2",
+ "log":{
+ "handler": {
+ "type" : "file",
+ "path" : "/tmp/log_info"
+ },
+ "actions":{
+ "open" :{
+ "type" : "systemd",
+ "unit" : "absolute",
+ "mode" : "replace-fake"
+ }
+ }
+ }
+ }
+ ]
+ )"_json;
+ auto h = LogHandlersBuilder().buildHandlerFromJson(j2);
+ ASSERT_THAT(h, ::testing::SizeIs(2));
+
+ EXPECT_FALSE(h[0].handler == nullptr);
+ EXPECT_FALSE(h[0].actions == nullptr);
+ EXPECT_THAT(h[0].blobId, "/log/blob1");
+ auto onOpen0 = reinterpret_cast<SystemdNoFile*>(h[0].actions->onOpen.get());
+ EXPECT_THAT(onOpen0->getMode(), "replace-nope");
+
+ EXPECT_FALSE(h[1].handler == nullptr);
+ EXPECT_FALSE(h[1].actions == nullptr);
+ EXPECT_THAT(h[1].blobId, "/log/blob2");
+ auto onOpen1 = reinterpret_cast<SystemdNoFile*>(h[1].actions->onOpen.get());
+ EXPECT_THAT(onOpen1->getMode(), "replace-fake");
+}
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_mock.hpp b/bmc/log-handler/test/log_mock.hpp
new file mode 100644
index 0000000..e9628eb
--- /dev/null
+++ b/bmc/log-handler/test/log_mock.hpp
@@ -0,0 +1,61 @@
+// Copyright 2021 Google Inc.
+//
+// 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
+//
+// http://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 "handler_config.hpp"
+#include "image_mock.hpp"
+#include "log_handler.hpp"
+#include "triggerable_mock.hpp"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+namespace ipmi_flash
+{
+
+auto createMockLogConfig(const std::string& id, ImageHandlerMock** im = nullptr,
+ TriggerMock** tm = nullptr)
+{
+ HandlerConfig<LogBlobHandler::ActionPack> ret;
+ ret.blobId = id;
+ auto handler = std::make_unique<testing::StrictMock<ImageHandlerMock>>();
+ if (im != nullptr)
+ {
+ *im = handler.get();
+ }
+ ret.handler = std::move(handler);
+ ret.actions = std::make_unique<LogBlobHandler::ActionPack>();
+ auto trigger = std::make_unique<testing::StrictMock<TriggerMock>>();
+ if (tm != nullptr)
+ {
+ *tm = trigger.get();
+ }
+ ret.actions->onOpen = std::move(trigger);
+ return ret;
+}
+
+template <typename C, typename Im = std::map<std::string, ImageHandlerMock*>,
+ typename Tm = std::map<std::string, TriggerMock*>>
+auto createMockLogConfigs(const C& ids, Im* im = nullptr, Tm* tm = nullptr)
+{
+ std::vector<HandlerConfig<LogBlobHandler::ActionPack>> ret;
+ for (const auto& id : ids)
+ {
+ ret.push_back(
+ createMockLogConfig(id, im == nullptr ? nullptr : &(*im)[id],
+ tm == nullptr ? nullptr : &(*tm)[id]));
+ }
+ return ret;
+}
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_open_unittest.cpp b/bmc/log-handler/test/log_open_unittest.cpp
new file mode 100644
index 0000000..301da86
--- /dev/null
+++ b/bmc/log-handler/test/log_open_unittest.cpp
@@ -0,0 +1,101 @@
+// Copyright 2021 Google Inc.
+//
+// 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
+//
+// http://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 "log_handler.hpp"
+#include "log_mock.hpp"
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::Return;
+
+namespace ipmi_flash
+{
+
+class LogOpenBlobTest : public ::testing::Test
+{
+ protected:
+ void SetUp() override
+ {
+ h = std::make_unique<LogBlobHandler>(
+ createMockLogConfigs(blobNames, &im, &tm));
+ }
+
+ std::unique_ptr<blobs::GenericBlobInterface> h;
+ std::vector<std::string> blobNames{"blob0", "blob1", "blob2", "blob3"};
+ std::unordered_map<std::string, TriggerMock*> tm;
+ std::unordered_map<std::string, ImageHandlerMock*> im;
+ const std::uint16_t defaultSessionNumber{0};
+};
+
+TEST_F(LogOpenBlobTest, VerifySingleBlobOpen)
+{
+ EXPECT_CALL(*tm.at("blob0"), trigger()).Times(1).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+}
+
+TEST_F(LogOpenBlobTest, VerifyMultipleBlobOpens)
+{
+ for (const auto& [_, val] : tm)
+ {
+ /* set the expectation that every onOpen will be triggered */
+ EXPECT_CALL(*val, trigger()).WillOnce(Return(true));
+ }
+ int i{defaultSessionNumber};
+ for (const auto& blob : blobNames)
+ {
+ EXPECT_TRUE(h->open(i++, blobs::read, blob));
+ }
+}
+
+TEST_F(LogOpenBlobTest, VerifyOpenAfterClose)
+{
+ EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+
+ EXPECT_CALL(*tm.at("blob0"), abort()).Times(1);
+ EXPECT_TRUE(h->close(defaultSessionNumber));
+
+ EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+}
+
+TEST_F(LogOpenBlobTest, VerifyMultiOpenWorks)
+{
+ EXPECT_CALL(*tm.at("blob1"), trigger()).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(0, blobs::read, "blob1"));
+ EXPECT_TRUE(h->open(1, blobs::read, "blob1"));
+ EXPECT_TRUE(h->open(2, blobs::read, "blob1"));
+}
+
+TEST_F(LogOpenBlobTest, VerifyFailedTriggerFails)
+{
+ EXPECT_CALL(*tm.at("blob1"), trigger()).WillOnce(Return(false));
+ EXPECT_FALSE(h->open(0, blobs::read, "blob1"));
+ EXPECT_CALL(*tm.at("blob1"), trigger()).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(0, blobs::read, "blob1"));
+}
+
+TEST_F(LogOpenBlobTest, VerifyUnsupportedOpenFlagsFails)
+{
+ EXPECT_FALSE(h->open(0, blobs::write, "blob1"));
+ EXPECT_CALL(*tm.at("blob1"), trigger()).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(0, blobs::read, "blob1"));
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_read_unittest.cpp b/bmc/log-handler/test/log_read_unittest.cpp
new file mode 100644
index 0000000..ab952e6
--- /dev/null
+++ b/bmc/log-handler/test/log_read_unittest.cpp
@@ -0,0 +1,153 @@
+// Copyright 2021 Google Inc.
+//
+// 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
+//
+// http://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 "log_handler.hpp"
+#include "log_mock.hpp"
+
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::ElementsAreArray;
+using ::testing::Ge;
+using ::testing::IsEmpty;
+using ::testing::Return;
+
+namespace ipmi_flash
+{
+
+class LogReadBlobTest : public ::testing::Test
+{
+ protected:
+ void SetUp() override
+ {
+ h = std::make_unique<LogBlobHandler>(
+ createMockLogConfigs(blobNames, &im, &tm));
+ }
+ std::unique_ptr<blobs::GenericBlobInterface> h;
+ std::vector<std::string> blobNames{"blob0", "blob1", "blob2", "blob3"};
+ std::unordered_map<std::string, TriggerMock*> tm;
+ std::unordered_map<std::string, ImageHandlerMock*> im;
+ const std::uint16_t defaultSessionNumber{200};
+ std::vector<uint8_t> vector1{0xDE, 0xAD, 0xBE, 0xEF,
+ 0xBA, 0xDF, 0xEE, 0x0D};
+ std::vector<uint8_t> vector2{0xCE, 0xAD, 0xDE, 0xFF};
+};
+
+TEST_F(LogReadBlobTest, VerifyValidRead)
+{
+ testing::InSequence seq;
+ EXPECT_CALL(*tm.at("blob0"), trigger())
+ .WillOnce(DoAll([&]() { tm.at("blob0")->cb(*tm.at("blob0")); },
+ Return(true)));
+ EXPECT_CALL(*tm.at("blob0"), status())
+ .WillOnce(Return(ActionStatus::success));
+ EXPECT_CALL(*im.at("blob0"), open(_, std::ios::in)).WillOnce(Return(true));
+ EXPECT_CALL(*im.at("blob0"), read(0, Ge(vector1.size())))
+ .WillOnce(Return(vector1));
+ EXPECT_CALL(*im.at("blob0"), close()).Times(1);
+ EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+
+ std::basic_string_view<uint8_t> vectorS(vector1.data(), vector1.size());
+ EXPECT_THAT(h->read(defaultSessionNumber, 0, 7),
+ ElementsAreArray(vectorS.substr(0, 7)));
+ EXPECT_THAT(h->read(defaultSessionNumber, 2, 10),
+ ElementsAreArray(vectorS.substr(2, 6)));
+ EXPECT_THAT(h->read(defaultSessionNumber, 10, 0), IsEmpty());
+}
+
+TEST_F(LogReadBlobTest, VerifyMultipleSession)
+{
+ testing::InSequence seq;
+ EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+ EXPECT_TRUE(h->open(1, blobs::read, "blob0"));
+
+ EXPECT_CALL(*tm.at("blob0"), status())
+ .WillOnce(Return(ActionStatus::success));
+ EXPECT_CALL(*im.at("blob0"), open(_, std::ios::in)).WillOnce(Return(true));
+ EXPECT_CALL(*im.at("blob0"), read(0, Ge(vector1.size())))
+ .WillOnce(Return(vector1));
+ EXPECT_CALL(*im.at("blob0"), close()).Times(1);
+ tm.at("blob0")->cb(*tm.at("blob0"));
+
+ EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(2, blobs::read, "blob0"));
+
+ EXPECT_CALL(*tm.at("blob0"), status())
+ .WillOnce(Return(ActionStatus::success));
+ EXPECT_CALL(*im.at("blob0"), open(_, std::ios::in)).WillOnce(Return(true));
+ EXPECT_CALL(*im.at("blob0"), read(0, Ge(vector2.size())))
+ .WillOnce(Return(vector2));
+ EXPECT_CALL(*im.at("blob0"), close()).Times(1);
+ tm.at("blob0")->cb(*tm.at("blob0"));
+
+ EXPECT_THAT(h->read(0, 0, 10), ElementsAreArray(vector1));
+ EXPECT_THAT(h->read(1, 0, 10), ElementsAreArray(vector1));
+ EXPECT_THAT(h->read(2, 0, 10), ElementsAreArray(vector2));
+}
+
+TEST_F(LogReadBlobTest, VerifyReadEarlyFails)
+{
+ EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+
+ EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+ EXPECT_THROW(h->read(defaultSessionNumber, 0, 10), std::runtime_error);
+}
+
+TEST_F(LogReadBlobTest, VerifyTriggerFailureReadFails)
+{
+ EXPECT_CALL(*tm.at("blob0"), trigger())
+ .WillOnce(DoAll([&]() { tm.at("blob0")->cb(*tm.at("blob0")); },
+ Return(true)));
+ EXPECT_CALL(*tm.at("blob0"), status())
+ .WillOnce(Return(ActionStatus::failed));
+ EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+ EXPECT_THROW(h->read(defaultSessionNumber, 0, 10), std::runtime_error);
+}
+
+TEST_F(LogReadBlobTest, VerifyReadFailsOnFileOpenFailure)
+{
+ EXPECT_CALL(*tm.at("blob0"), trigger())
+ .WillOnce(DoAll([&]() { tm.at("blob0")->cb(*tm.at("blob0")); },
+ Return(true)));
+ EXPECT_CALL(*tm.at("blob0"), status())
+ .WillOnce(Return(ActionStatus::success));
+ EXPECT_CALL(*im.at("blob0"), open(_, std::ios::in)).WillOnce(Return(false));
+
+ EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+ EXPECT_THROW(h->read(defaultSessionNumber, 0, 10), std::runtime_error);
+}
+
+TEST_F(LogReadBlobTest, VerifyReadFailsOnFileReadFailure)
+{
+ EXPECT_CALL(*tm.at("blob0"), trigger())
+ .WillOnce(DoAll([&]() { tm.at("blob0")->cb(*tm.at("blob0")); },
+ Return(true)));
+ EXPECT_CALL(*tm.at("blob0"), status())
+ .WillOnce(Return(ActionStatus::success));
+ EXPECT_CALL(*im.at("blob0"), open(_, std::ios::in)).WillOnce(Return(true));
+ EXPECT_CALL(*im.at("blob0"), read(_, _)).WillOnce(Return(std::nullopt));
+ EXPECT_CALL(*im.at("blob0"), close()).Times(1);
+
+ EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+ EXPECT_THROW(h->read(defaultSessionNumber, 0, 10), std::runtime_error);
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_stat_unittest.cpp b/bmc/log-handler/test/log_stat_unittest.cpp
new file mode 100644
index 0000000..6eb91b5
--- /dev/null
+++ b/bmc/log-handler/test/log_stat_unittest.cpp
@@ -0,0 +1,95 @@
+// Copyright 2021 Google Inc.
+//
+// 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
+//
+// http://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 "log_handler.hpp"
+#include "log_mock.hpp"
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::Return;
+
+namespace ipmi_flash
+{
+
+class LogStatBlobTest : public ::testing::Test
+{
+ protected:
+ void SetUp() override
+ {
+ h = std::make_unique<LogBlobHandler>(
+ createMockLogConfigs(blobNames, &im, &tm));
+
+ EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+ EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(h->stat(0, &meta));
+ EXPECT_EQ(blobs::StateFlags::committing, meta.blobState);
+ }
+
+ std::unique_ptr<blobs::GenericBlobInterface> h;
+ std::vector<std::string> blobNames{"blob0"};
+ std::unordered_map<std::string, TriggerMock*> tm;
+ std::unordered_map<std::string, ImageHandlerMock*> im;
+};
+
+TEST_F(LogStatBlobTest, CreateError)
+{
+ EXPECT_CALL(*tm.at("blob0"), status())
+ .WillOnce(Return(ActionStatus::failed));
+ tm.at("blob0")->cb(*tm.at("blob0"));
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(h->stat(0, &meta));
+ EXPECT_EQ(blobs::StateFlags::commit_error, meta.blobState);
+}
+
+class LogStatSizeBlobTest :
+ public LogStatBlobTest,
+ public ::testing::WithParamInterface<std::vector<uint8_t>>
+{};
+
+TEST_P(LogStatSizeBlobTest, StatWithSize)
+{
+ const std::vector<uint8_t> data = GetParam();
+ EXPECT_CALL(*tm.at("blob0"), status())
+ .WillOnce(Return(ActionStatus::success));
+ EXPECT_CALL(*im.at("blob0"), open(_, std::ios::in)).WillOnce(Return(true));
+ EXPECT_CALL(*im.at("blob0"), read(0, ::testing::Ge(data.size())))
+ .WillOnce(Return(data));
+ EXPECT_CALL(*im.at("blob0"), close()).Times(1);
+ tm.at("blob0")->cb(*tm.at("blob0"));
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(h->stat(0, &meta));
+ EXPECT_EQ(blobs::StateFlags::committed | blobs::StateFlags::open_read,
+ meta.blobState);
+ EXPECT_EQ(data.size(), meta.size);
+}
+
+const std::vector<std::vector<uint8_t>> datas = {
+ {},
+ {0, 1, 2, 3, 4, 5, 6},
+};
+
+INSTANTIATE_TEST_SUITE_P(DifferentData, LogStatSizeBlobTest,
+ testing::ValuesIn(datas));
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/meson.build b/bmc/log-handler/test/meson.build
new file mode 100644
index 0000000..36b4917
--- /dev/null
+++ b/bmc/log-handler/test/meson.build
@@ -0,0 +1,14 @@
+log_tests = [
+ 'canhandle_enumerate',
+ 'createhandler']
+
+foreach t : log_tests
+ test(
+ t,
+ executable(
+ t.underscorify(), 'log_' + t + '_unittest.cpp',
+ build_by_default: false,
+ implicit_include_directories: false,
+ include_directories: [root_inc, bmc_test_inc, log_inc],
+ dependencies: [log_dep, blobs_dep, gtest, gmock]))
+endforeach