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/README.md b/bmc/README.md
new file mode 100644
index 0000000..261b539
--- /dev/null
+++ b/bmc/README.md
@@ -0,0 +1,74 @@
+# Format of Config file
+
+This document gives details about the format of the config file useed by log, version and firmware handler. The config file is a .json file.
+
+### Parameters
+
+There are 3 important parameters in this config file
+1. blob
+2. handler
+3. actions
+
+An example config file -
+
+```
+{
+ "blob": "/flash/adm1266_sink0",
+ "handler": {
+ "type": "file",
+ "path": "/var/run/adm1266/adm1266_sink0.hex"
+ },
+ "actions": {
+ "preparation": {
+ "type": "skip"
+ },
+ "verification": {
+ "type": "systemd",
+ "unit": "adm1266-verify@sink0.service"
+ },
+ "update": {
+ "type": "systemd",
+ "unit": "adm1266-update@sink0.service"
+ }
+ }
+}
+```
+
+#### blob
+This parameter defines the unique name of the blob. This parameter must be defined for each blob.
+
+#### handler
+A blob must have a handler with a type. Currently only "file" type is supported. With file type, a path of the handler file must be provided.
+
+#### actions
+"actions" define various steps to be performed and are required for each blob. These actions can be triggered from ipmi-blob commands. Currently there are 2 types of actions supported: `systemd` to invoke a systemd service, or `skip` to not perform any action.
+
+### Workflow of log handler
+
+```
+{
+ "blob": "/log/blackbox_adm1266_sink0",
+ "handler": {
+ "type": "file",
+ "path": "/var/run/adm1266/adm1266_sink0.log"
+ },
+ "actions":{
+ "open": {
+ "type": "systemd",
+ "unit": "adm1266-read-blackbox-log@sink0.service"
+ },
+ "delete": {
+ "type": "systemd",
+ "unit": "adm1266-clear-blackbox-data@sink0.service"
+ }
+ }
+}
+```
+
+In this example the blob handler is the log file that will store the blackbox data from adm1266 and will be returned on BmcBlobRead.
+
+Here log_blob supports 2 actions. These actions are performed on the handler file.
+
+1. `open` : adm1266-read-blackbox-log@sink0.service gets launched on BmcBlobOpen command. This service should read the blackbox data from adm1266 and place the into blob handler file. This also enables BmcBlobSessionStat command to indicate that the blob is ready to read.
+
+2. `delete` : adm1266-clear-blackbox-data@sink0.service gets launched on BmcBlobDelete command. This service should delete the cached blackbox data in the handler file and erase the blackbox data from adm1266.
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
diff --git a/bmc/meson.build b/bmc/meson.build
index 0ccd3c2..a65d888 100644
--- a/bmc/meson.build
+++ b/bmc/meson.build
@@ -22,4 +22,5 @@
endif
subdir('firmware-handler')
-subdir('version-handler')
\ No newline at end of file
+subdir('version-handler')
+subdir('log-handler')
\ No newline at end of file