Add the stream mode
The stream mode behaves differently in versus the existing buffer mode.
1. It leverages rsyslog to persist logs;
2. It leverages logrotate to rotate and compress logs;
3. It persists logs as soon as they are collected.
Add configuration options to choose modes at start up time. When stream
mode is disabled, no difference compared to the existing service.
See README.md for details.
This change also adds mock classes for unit test purpose.
Change-Id: Ic7d02e826c7d9372621c096c6e768e6216974150
Signed-off-by: Nan Zhou <nanzhoumails@gmail.com>
diff --git a/test/buffer_service_test.cpp b/test/buffer_service_test.cpp
new file mode 100644
index 0000000..602dfcc
--- /dev/null
+++ b/test/buffer_service_test.cpp
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2021 Google
+
+#include "buffer_service.hpp"
+#include "config.hpp"
+#include "dbus_loop_mock.hpp"
+#include "file_storage_mock.hpp"
+#include "host_console_mock.hpp"
+#include "log_buffer_mock.hpp"
+
+#include <memory>
+#include <string>
+#include <system_error>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace
+{
+
+constexpr char firstDatagram[] = "Hello world";
+// Shouldn't read more than maximum size of a datagram.
+constexpr int consoleReadMaxSize = 1024;
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::Eq;
+using ::testing::InSequence;
+using ::testing::Le;
+using ::testing::Ref;
+using ::testing::Return;
+using ::testing::SetArrayArgument;
+using ::testing::StrEq;
+using ::testing::Test;
+using ::testing::Throw;
+
+// A helper class that owns config.
+struct ConfigInTest
+{
+ Config config;
+ ConfigInTest() : config()
+ {}
+};
+
+class BufferServiceTest : public Test, public ConfigInTest, public BufferService
+{
+ public:
+ // ConfigInTest::config is initialized before BufferService.
+ BufferServiceTest() :
+ BufferService(ConfigInTest::config, dbusLoopMock, hostConsoleMock,
+ logBufferMock, fileStorageMock)
+ {}
+
+ MOCK_METHOD(void, flush, (), (override));
+ MOCK_METHOD(void, readConsole, (), (override));
+
+ protected:
+ // Set hostConsole firstly read specified data and then read nothing.
+ void setHostConsoleOnce(char const* data, size_t len)
+ {
+ EXPECT_CALL(hostConsoleMock, read(_, Le(consoleReadMaxSize)))
+ .WillOnce(DoAll(SetArrayArgument<0>(data, data + len), Return(len)))
+ .WillOnce(Return(0));
+ }
+
+ DbusLoopMock dbusLoopMock;
+ HostConsoleMock hostConsoleMock;
+ LogBufferMock logBufferMock;
+ FileStorageMock fileStorageMock;
+};
+
+TEST_F(BufferServiceTest, FlushEmptyBuffer)
+{
+ EXPECT_CALL(logBufferMock, empty()).WillOnce(Return(true));
+ EXPECT_NO_THROW(BufferService::flush());
+}
+
+TEST_F(BufferServiceTest, FlushExceptionCaught)
+{
+ InSequence sequence;
+ EXPECT_CALL(logBufferMock, empty()).WillOnce(Return(false));
+ EXPECT_CALL(fileStorageMock, save(Ref(logBufferMock)))
+ .WillOnce(Throw(std::runtime_error("Mock error")));
+ EXPECT_NO_THROW(BufferService::flush());
+}
+
+TEST_F(BufferServiceTest, FlushOk)
+{
+ InSequence sequence;
+ EXPECT_CALL(logBufferMock, empty()).WillOnce(Return(false));
+ EXPECT_CALL(fileStorageMock, save(Ref(logBufferMock)));
+ EXPECT_CALL(logBufferMock, clear());
+ EXPECT_NO_THROW(BufferService::flush());
+}
+
+TEST_F(BufferServiceTest, ReadConsoleExceptionCaught)
+{
+ InSequence sequence;
+ // Shouldn't read more than maximum size of a datagram.
+ EXPECT_CALL(hostConsoleMock, read(_, Le(1024)))
+ .WillOnce(Throw(std::system_error(std::error_code(), "Mock error")));
+ EXPECT_NO_THROW(BufferService::readConsole());
+}
+
+TEST_F(BufferServiceTest, ReadConsoleOk)
+{
+
+ setHostConsoleOnce(firstDatagram, strlen(firstDatagram));
+ EXPECT_CALL(logBufferMock,
+ append(StrEq(firstDatagram), Eq(strlen(firstDatagram))))
+ .WillOnce(Return());
+ EXPECT_NO_THROW(BufferService::readConsole());
+}
+
+TEST_F(BufferServiceTest, RunIoRegisterError)
+{
+ EXPECT_CALL(hostConsoleMock, connect()).WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, addSignalHandler(Eq(SIGUSR1), _))
+ .WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, addSignalHandler(Eq(SIGTERM), _))
+ .WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, addIoHandler(Eq(int(hostConsoleMock)), _))
+ .WillOnce(Throw(std::runtime_error("Mock error")));
+ EXPECT_THROW(run(), std::runtime_error);
+}
+
+TEST_F(BufferServiceTest, RunSignalRegisterError)
+{
+ EXPECT_CALL(hostConsoleMock, connect()).WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, addSignalHandler(Eq(SIGUSR1), _))
+ .WillOnce(Throw(std::runtime_error("Mock error")));
+ EXPECT_THROW(run(), std::runtime_error);
+}
+
+TEST_F(BufferServiceTest, RunOk)
+{
+ ConfigInTest::config.bufFlushFull = true;
+ EXPECT_CALL(hostConsoleMock, connect()).WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, addIoHandler(Eq(int(hostConsoleMock)), _))
+ .WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, addSignalHandler(Eq(SIGTERM), _))
+ .WillOnce(Return());
+ EXPECT_CALL(logBufferMock, setFullHandler(_)).WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, addSignalHandler(Eq(SIGUSR1), _))
+ .WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock,
+ addPropertyHandler(StrEq(ConfigInTest::config.hostState), _, _))
+ .WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, run).WillOnce(Return(0));
+ EXPECT_CALL(logBufferMock, empty()).WillOnce(Return(false));
+ EXPECT_CALL(*this, flush()).WillOnce(Return());
+ EXPECT_NO_THROW(run());
+}
+} // namespace
diff --git a/test/config_test.cpp b/test/config_test.cpp
index 15fbc4b..65936f5 100644
--- a/test/config_test.cpp
+++ b/test/config_test.cpp
@@ -3,16 +3,20 @@
#include "config.hpp"
+#include <sys/un.h>
+
#include <gtest/gtest.h>
// Names of environment variables
static const char* SOCKET_ID = "SOCKET_ID";
+static const char* MODE = "MODE";
static const char* BUF_MAXSIZE = "BUF_MAXSIZE";
static const char* BUF_MAXTIME = "BUF_MAXTIME";
static const char* FLUSH_FULL = "FLUSH_FULL";
static const char* HOST_STATE = "HOST_STATE";
static const char* OUT_DIR = "OUT_DIR";
static const char* MAX_FILES = "MAX_FILES";
+static const char* STREAM_DST = "STREAM_DST";
/**
* @class ConfigTest
@@ -35,12 +39,14 @@
void resetEnv() const
{
unsetenv(SOCKET_ID);
+ unsetenv(MODE);
unsetenv(BUF_MAXSIZE);
unsetenv(BUF_MAXTIME);
unsetenv(FLUSH_FULL);
unsetenv(HOST_STATE);
unsetenv(OUT_DIR);
unsetenv(MAX_FILES);
+ unsetenv(STREAM_DST);
}
};
@@ -48,17 +54,20 @@
{
Config cfg;
EXPECT_STREQ(cfg.socketId, "");
+ EXPECT_EQ(cfg.mode, Mode::bufferMode);
EXPECT_EQ(cfg.bufMaxSize, 3000);
EXPECT_EQ(cfg.bufMaxTime, 0);
EXPECT_EQ(cfg.bufFlushFull, false);
EXPECT_STREQ(cfg.hostState, "/xyz/openbmc_project/state/host0");
EXPECT_STREQ(cfg.outDir, "/var/lib/obmc/hostlogs");
EXPECT_EQ(cfg.maxFiles, 10);
+ EXPECT_STREQ(cfg.streamDestination, "/run/rsyslog/console_input");
}
-TEST_F(ConfigTest, Load)
+TEST_F(ConfigTest, LoadInBufferMode)
{
setenv(SOCKET_ID, "id123", 1);
+ setenv(MODE, "buffer", 1);
setenv(BUF_MAXSIZE, "1234", 1);
setenv(BUF_MAXTIME, "4321", 1);
setenv(FLUSH_FULL, "true", 1);
@@ -68,30 +77,73 @@
Config cfg;
EXPECT_STREQ(cfg.socketId, "id123");
+ EXPECT_EQ(cfg.mode, Mode::bufferMode);
EXPECT_EQ(cfg.bufMaxSize, 1234);
EXPECT_EQ(cfg.bufMaxTime, 4321);
EXPECT_EQ(cfg.bufFlushFull, true);
EXPECT_STREQ(cfg.hostState, "host123");
EXPECT_STREQ(cfg.outDir, "path123");
EXPECT_EQ(cfg.maxFiles, 1122);
+ // This should be default.
+ EXPECT_STREQ(cfg.streamDestination, "/run/rsyslog/console_input");
+}
+
+TEST_F(ConfigTest, LoadInStreamMode)
+{
+ setenv(SOCKET_ID, "id123", 1);
+ setenv(MODE, "stream", 1);
+ setenv(STREAM_DST, "path123", 1);
+
+ Config cfg;
+ EXPECT_STREQ(cfg.socketId, "id123");
+ EXPECT_EQ(cfg.mode, Mode::streamMode);
+ EXPECT_STREQ(cfg.streamDestination, "path123");
+
+ // These should be default.
+ EXPECT_EQ(cfg.bufMaxSize, 3000);
+ EXPECT_EQ(cfg.bufMaxTime, 0);
+ EXPECT_EQ(cfg.bufFlushFull, false);
+ EXPECT_STREQ(cfg.hostState, "/xyz/openbmc_project/state/host0");
+ EXPECT_STREQ(cfg.outDir, "/var/lib/obmc/hostlogs");
+ EXPECT_EQ(cfg.maxFiles, 10);
}
TEST_F(ConfigTest, InvalidNumeric)
{
setenv(BUF_MAXSIZE, "-1234", 1);
- ASSERT_THROW(Config(), std::invalid_argument);
+ EXPECT_THROW(Config(), std::invalid_argument);
}
TEST_F(ConfigTest, InvalidBoolean)
{
setenv(FLUSH_FULL, "invalid", 1);
- ASSERT_THROW(Config(), std::invalid_argument);
+ EXPECT_THROW(Config(), std::invalid_argument);
+ setenv(FLUSH_FULL, "true", 1);
+ EXPECT_NO_THROW(Config());
}
-TEST_F(ConfigTest, InvalidConfig)
+TEST_F(ConfigTest, Mode)
+{
+ setenv(MODE, "invalid", 1);
+ EXPECT_THROW(Config(), std::invalid_argument);
+ setenv(MODE, "stream", 1);
+ EXPECT_EQ(Config().mode, Mode::streamMode);
+ setenv(MODE, "buffer", 1);
+ EXPECT_EQ(Config().mode, Mode::bufferMode);
+}
+
+TEST_F(ConfigTest, InvalidBufferModeConfig)
{
setenv(BUF_MAXSIZE, "0", 1);
setenv(BUF_MAXTIME, "0", 1);
setenv(FLUSH_FULL, "true", 1);
- ASSERT_THROW(Config(), std::invalid_argument);
+ EXPECT_THROW(Config(), std::invalid_argument);
+}
+
+TEST_F(ConfigTest, InvalidStreamModeConfig)
+{
+ std::string tooLong(sizeof(sockaddr_un::sun_path), '0');
+ setenv(MODE, "stream", 1);
+ setenv(STREAM_DST, tooLong.c_str(), 1);
+ EXPECT_THROW(Config(), std::invalid_argument);
}
diff --git a/test/dbus_loop_mock.hpp b/test/dbus_loop_mock.hpp
new file mode 100644
index 0000000..c8183fa
--- /dev/null
+++ b/test/dbus_loop_mock.hpp
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2021 GOOGLE
+
+#pragma once
+
+#include "dbus_loop.hpp"
+
+#include <gmock/gmock.h>
+
+class DbusLoopMock : public DbusLoop
+{
+ public:
+ MOCK_METHOD(int, run, (), (const, override));
+ MOCK_METHOD(void, addIoHandler, (int fd, std::function<void()> callback),
+ (override));
+ MOCK_METHOD(void, addSignalHandler,
+ (int signal, std::function<void()> callback), (override));
+ MOCK_METHOD(void, addPropertyHandler,
+ (const std::string& objPath, const WatchProperties& props,
+ std::function<void()> callback),
+ (override));
+};
diff --git a/test/file_storage_mock.hpp b/test/file_storage_mock.hpp
new file mode 100644
index 0000000..449ab29
--- /dev/null
+++ b/test/file_storage_mock.hpp
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2021 GOOGLE
+
+#pragma once
+
+#include "file_storage.hpp"
+
+#include <gmock/gmock.h>
+
+class FileStorageMock : public FileStorage
+{
+ public:
+ FileStorageMock() : FileStorage("/tmp", "fake", -1)
+ {}
+ MOCK_METHOD(std::string, save, (const LogBuffer& buf), (const override));
+};
diff --git a/test/host_console_mock.hpp b/test/host_console_mock.hpp
new file mode 100644
index 0000000..3b71c3c
--- /dev/null
+++ b/test/host_console_mock.hpp
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2021 GOOGLE
+
+#pragma once
+
+#include "host_console.hpp"
+
+#include <gmock/gmock.h>
+
+class HostConsoleMock : public HostConsole
+{
+ public:
+ HostConsoleMock() : HostConsole("")
+ {}
+ MOCK_METHOD(void, connect, (), (override));
+ MOCK_METHOD(size_t, read, (char* buf, size_t sz), (const, override));
+ // Returns a fixed integer for testing.
+ virtual operator int() const override
+ {
+ return 1;
+ };
+};
diff --git a/test/log_buffer_mock.hpp b/test/log_buffer_mock.hpp
new file mode 100644
index 0000000..f53fa87
--- /dev/null
+++ b/test/log_buffer_mock.hpp
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2021 GOOGLE
+
+#pragma once
+
+#include "log_buffer.hpp"
+
+#include <gmock/gmock.h>
+
+class LogBufferMock : public LogBuffer
+{
+ public:
+ LogBufferMock() : LogBuffer(-1, -1)
+ {}
+ MOCK_METHOD(void, append, (const char* data, size_t sz), (override));
+ MOCK_METHOD(void, setFullHandler, (std::function<void()> cb), (override));
+ MOCK_METHOD(bool, empty, (), (const, override));
+ MOCK_METHOD(void, clear, (), (override));
+};
diff --git a/test/meson.build b/test/meson.build
index 1a10b6f..9916157 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -9,18 +9,26 @@
'file_storage_test.cpp',
'host_console_test.cpp',
'log_buffer_test.cpp',
+ 'buffer_service_test.cpp',
+ 'stream_service_test.cpp',
'zlib_file_test.cpp',
+ '../src/buffer_service.cpp',
'../src/config.cpp',
+ '../src/dbus_loop.cpp',
'../src/file_storage.cpp',
'../src/host_console.cpp',
'../src/log_buffer.cpp',
+ '../src/stream_service.cpp',
'../src/zlib_exception.cpp',
'../src/zlib_file.cpp',
],
dependencies: [
dependency('gtest', main: true, disabler: true, required: build_tests),
+ dependency('gmock', disabler: true, required: build_tests),
dependency('zlib'),
+ dependency('phosphor-logging'),
],
+ cpp_args : ['-DSTREAM_SERVICE', '-DBUFFER_SERVICE'],
include_directories: '../src',
)
)
diff --git a/test/stream_service_test.cpp b/test/stream_service_test.cpp
new file mode 100644
index 0000000..ab62c78
--- /dev/null
+++ b/test/stream_service_test.cpp
@@ -0,0 +1,160 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2021 Google
+
+#include "config.hpp"
+#include "dbus_loop_mock.hpp"
+#include "host_console_mock.hpp"
+#include "stream_service.hpp"
+
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include <memory>
+#include <string>
+#include <system_error>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace
+{
+
+constexpr char socketPath[] = "\0rsyslog";
+constexpr char firstDatagram[] = "Hello world";
+constexpr char secondDatagram[] = "World hello again";
+// Shouldn't read more than maximum size of a datagram.
+constexpr int consoleReadMaxSize = 1024;
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::Eq;
+using ::testing::InSequence;
+using ::testing::Le;
+using ::testing::Ref;
+using ::testing::Return;
+using ::testing::SetArrayArgument;
+using ::testing::StrEq;
+using ::testing::Test;
+using ::testing::Throw;
+
+class StreamServiceTest : public Test, public StreamService
+{
+ public:
+ StreamServiceTest() :
+ StreamService(socketPath, dbusLoopMock, hostConsoleMock),
+ serverSocket(-1)
+ {}
+ ~StreamServiceTest() override
+ {
+ // Stop server
+ if (serverSocket != -1)
+ {
+ close(serverSocket);
+ }
+ }
+
+ MOCK_METHOD(void, readConsole, (), (override));
+ MOCK_METHOD(void, streamConsole, (const char* data, size_t len),
+ (override));
+ MOCK_METHOD(void, setStreamSocket, (), (override));
+
+ protected:
+ // Start a server for reading datagrams.
+ void startServer()
+ {
+ serverSocket = socket(AF_UNIX, SOCK_DGRAM, 0);
+ ASSERT_NE(serverSocket, -1);
+ sockaddr_un sa{};
+ sa.sun_family = AF_UNIX;
+ memcpy(sa.sun_path, socketPath, sizeof(socketPath) - 1);
+ sa.sun_path[sizeof(socketPath) - 1] = '\0';
+ const socklen_t len =
+ sizeof(sa) - sizeof(sa.sun_path) + sizeof(socketPath) - 1;
+ ASSERT_NE(
+ bind(serverSocket, reinterpret_cast<const sockaddr*>(&sa), len),
+ -1);
+ }
+
+ // Set hostConsole firstly read specified data and then read nothing.
+ void setHostConsoleOnce(char const* data, size_t len)
+ {
+ EXPECT_CALL(hostConsoleMock, read(_, Le(consoleReadMaxSize)))
+ .WillOnce(DoAll(SetArrayArgument<0>(data, data + len), Return(len)))
+ .WillOnce(Return(0));
+ }
+
+ DbusLoopMock dbusLoopMock;
+ HostConsoleMock hostConsoleMock;
+ int serverSocket;
+};
+
+TEST_F(StreamServiceTest, ReadConsoleExceptionCaught)
+{
+ InSequence sequence;
+ // Shouldn't read more than maximum size of a datagram.
+ EXPECT_CALL(hostConsoleMock, read(_, Le(1024)))
+ .WillOnce(Throw(std::system_error(std::error_code(), "Mock error")));
+ EXPECT_NO_THROW(StreamService::readConsole());
+}
+
+TEST_F(StreamServiceTest, ReadConsoleOk)
+{
+ // stream mode
+ setHostConsoleOnce(firstDatagram, strlen(firstDatagram));
+ EXPECT_CALL(*this,
+ streamConsole(StrEq(firstDatagram), Eq(strlen(firstDatagram))))
+ .WillOnce(Return());
+ EXPECT_NO_THROW(StreamService::readConsole());
+}
+
+TEST_F(StreamServiceTest, StreamConsoleOk)
+{
+ startServer();
+ EXPECT_NO_THROW(StreamService::setStreamSocket());
+ EXPECT_NO_THROW(
+ StreamService::streamConsole(firstDatagram, strlen(firstDatagram)));
+ EXPECT_NO_THROW(
+ StreamService::streamConsole(secondDatagram, strlen(secondDatagram)));
+ char buffer[consoleReadMaxSize];
+ EXPECT_EQ(read(serverSocket, buffer, consoleReadMaxSize),
+ strlen(firstDatagram));
+ buffer[strlen(firstDatagram)] = '\0';
+ EXPECT_STREQ(buffer, firstDatagram);
+ EXPECT_EQ(read(serverSocket, buffer, consoleReadMaxSize),
+ strlen(secondDatagram));
+ buffer[strlen(secondDatagram)] = '\0';
+ EXPECT_STREQ(buffer, secondDatagram);
+}
+
+TEST_F(StreamServiceTest, RunIoRegisterError)
+{
+ EXPECT_CALL(*this, setStreamSocket()).WillOnce(Return());
+ EXPECT_CALL(hostConsoleMock, connect()).WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, addSignalHandler(Eq(SIGTERM), _))
+ .WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, addIoHandler(Eq(int(hostConsoleMock)), _))
+ .WillOnce(Throw(std::runtime_error("Mock error")));
+ EXPECT_THROW(run(), std::runtime_error);
+}
+
+TEST_F(StreamServiceTest, RunSignalRegisterError)
+{
+ EXPECT_CALL(*this, setStreamSocket()).WillOnce(Return());
+ EXPECT_CALL(hostConsoleMock, connect()).WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, addSignalHandler(Eq(SIGTERM), _))
+ .WillOnce(Throw(std::runtime_error("Mock error")));
+ EXPECT_THROW(run(), std::runtime_error);
+}
+
+TEST_F(StreamServiceTest, RunOk)
+{
+ EXPECT_CALL(hostConsoleMock, connect()).WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, addIoHandler(Eq(int(hostConsoleMock)), _))
+ .WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, addSignalHandler(Eq(SIGTERM), _))
+ .WillOnce(Return());
+ EXPECT_CALL(*this, setStreamSocket()).WillOnce(Return());
+ EXPECT_CALL(dbusLoopMock, run).WillOnce(Return(0));
+ EXPECT_NO_THROW(run());
+}
+} // namespace