blob: ad6fa281f237b75df01a9691a9fac73551f66417 [file] [log] [blame]
// Copyright 2024 Google LLC
//
// 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 "mtd_util_unittest.hpp"
#include "fs.hpp"
#include "mtd_util.hpp"
#include <fcntl.h>
#include <mtd/mtd-user.h>
#include <cstring>
#include <exception>
#include <filesystem>
#include <map>
#include <optional>
#include <random>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
// NOLINTNEXTLINE(google-build-using-namespace)
using namespace std::literals;
namespace fs = std::filesystem;
using ::testing::_;
using ::testing::AllOf;
using ::testing::ContainerEq;
using ::testing::DoAll;
using ::testing::Field;
using ::testing::Invoke;
using ::testing::MatcherCast;
using ::testing::NotNull;
using ::testing::Return;
using ::testing::SetErrnoAndReturn;
using ::testing::StrEq;
auto static const test_str = "Hello, world!"s;
std::vector<uint8_t> static const test_buf(test_str.begin(), test_str.end());
auto constexpr testName = "test-name";
std::map<fs::path, std::optional<std::string>> mtdMap;
std::vector<fs::path>
google::hoth::internal::listDir(const fs::path& /*unused*/)
{
std::vector<fs::path> output;
output.reserve(mtdMap.size());
for (const auto& p : mtdMap) {
output.push_back(p.first);
}
return output;
}
std::string google::hoth::internal::getFirstLine(const fs::path& path)
{
if (path.filename() != "name")
{
throw std::ios_base::failure("file not found");
}
auto result = mtdMap.at(path.parent_path().string());
try
{
return result.value();
}
catch (const std::bad_optional_access& e)
{
throw std::ios_base::failure("file not found");
}
}
// Custom action to typecast from void* to mtd_info_t* in our mock return
ACTION_P(SetArg2ToMtdPointer, value)
{
*static_cast<mtd_info_t*>(arg2) = value;
}
TEST(PartitionTest, emptyMtdMapThrowsException)
{
// Attempt to findPartition when mtdMap is empty, expect exception thrown
EXPECT_THROW(google::hoth::internal::mtdImpl.findPartition(testName),
std::exception);
}
TEST(PartitionTest, mtdMapDoesNotContainPathToFileThrowsException)
{
// Attempt to findPartition when mtdMap does not contain path to testName
// file, expect exception thrown
mtdMap = {{"/sys/class/mtd/mtd0", "b"}, {"/sys/class/mtd/mtd1", "c"}};
EXPECT_THROW(google::hoth::internal::mtdImpl.findPartition(testName),
std::exception);
}
TEST(PartitionTest, mtdMapContainsPathToFileReturnsPath)
{
// Attempt to findPartition when mtdMap contains a path with testName file
// and another path without, expect the path that contains the testName
// file to be returned
mtdMap = {{"/sys/class/mtd/mtd0", "b"}, {"/sys/class/mtd/mtd1", testName}};
EXPECT_EQ("/dev/mtd1",
google::hoth::internal::mtdImpl.findPartition(testName));
}
TEST(PartitionTest, mtdMapContainsPathToFileAndPathsWithoutFilesReturnsPath)
{
// Attempt to findPartition when mtdMap contains a path with testName file,
// path with a random file and mtd*ro folders which don't have a "name"
// file and will throw an exception when trying to read them. We expect
// the path that contains the testName file to be returned.
mtdMap = {{"/sys/class/mtd/mtd0", "b"},
{"/sys/class/mtd/mtd0ro", std::nullopt},
{"/sys/class/mtd/mtd1", testName},
{"/sys/class/mtd/mtd1ro", std::nullopt}};
EXPECT_EQ("/dev/mtd1",
google::hoth::internal::mtdImpl.findPartition(testName));
}
TEST_F(FlashCopyTest, openInvalidPartitionThrowsException)
{
// Attempt to flashCopy to an invalid partition, mock the return of the
// sys open call with ENOENT which is "No such file or directory".
// Expect exception thrown.
std::vector<uint8_t> data;
EXPECT_CALL(sys, open(StrEq(TEST_PATH), _))
.WillOnce(SetErrnoAndReturn(ENOENT, -1));
EXPECT_THROW(
google::hoth::internal::mtdImpl.flashCopy(&sys, data, TEST_PATH),
std::exception);
}
TEST_F(FlashCopyTest, partitionNotMtdThrowsException)
{
// Attempt to flashCopy to a partition which is not an MTD, mock the return
// of the ioctl MEMGETINFO call with ENOTTY which is "Inappropriate ioctl
// for device". Expect exception thrown.
std::vector<uint8_t> data;
EXPECT_CALL(sys, open(StrEq(TEST_PATH), _)).WillOnce(Return(TEST_FD));
EXPECT_CALL(sys, ioctl(TEST_FD, MEMGETINFO, NotNull()))
.WillOnce(SetErrnoAndReturn(ENOTTY, -1));
EXPECT_CALL(sys, close(TEST_FD)).WillOnce(Return(0));
EXPECT_THROW(
google::hoth::internal::mtdImpl.flashCopy(&sys, data, TEST_PATH),
std::exception);
}
TEST_F(FlashCopyTest, partitionTooSmallThrowsException)
{
// Attempt to flashCopy data bigger than the size of partition.
// Expect exception thrown.
std::vector<uint8_t> data(10);
mtd_info_t mtd;
// Choose mtd size smaller than data size
mtd.size = 5;
EXPECT_CALL(sys, open(StrEq(TEST_PATH), _)).WillOnce(Return(TEST_FD));
EXPECT_CALL(sys, ioctl(TEST_FD, MEMGETINFO, NotNull()))
.WillOnce(DoAll(SetArg2ToMtdPointer(mtd), Return(0)));
EXPECT_CALL(sys, close(TEST_FD)).WillOnce(Return(0));
EXPECT_THROW(
google::hoth::internal::mtdImpl.flashCopy(&sys, data, TEST_PATH),
std::exception);
}
TEST_F(FlashCopyTest, eraseNotSupportedThrowsException)
{
// Attempt to flashCopy with valid mtd and erase_info_t but mock
// ioctl MEMERASE to return "ENOTSUP" which is "Not Supported", meaning the
// MTD does not support the memerase functionality. Expect exception thrown.
std::vector<uint8_t> data(10);
mtd_info_t mtd;
// Choose a valid mtd size (value bigger than data size)
mtd.size = 16;
EXPECT_CALL(sys, open(StrEq(TEST_PATH), _)).WillOnce(Return(TEST_FD));
EXPECT_CALL(sys, ioctl(TEST_FD, MEMGETINFO, NotNull()))
.WillOnce(DoAll(SetArg2ToMtdPointer(mtd), Return(0)));
EXPECT_CALL(sys, ioctl(TEST_FD, MEMERASE,
MatcherCast<void*>(MatcherCast<erase_info_t*>(
AllOf(Field(&erase_info_t::start, 0),
Field(&erase_info_t::length, mtd.size))))))
.WillOnce(SetErrnoAndReturn(ENOTSUP, -1));
EXPECT_CALL(sys, close(TEST_FD)).WillOnce(Return(0));
EXPECT_THROW(
google::hoth::internal::mtdImpl.flashCopy(&sys, data, TEST_PATH),
std::exception);
}
class FakeMtd
{
public:
explicit FakeMtd(uint32_t flags = 0) : flags(flags)
{
mtd.type = MTD_NORFLASH;
mtd.flags = MTD_WRITEABLE;
mtd.size = deviceSize;
mtd.erasesize = eraseSize;
mtd.writesize = 1;
data.resize(deviceSize);
if (flags & flagWriteIntr)
{
writeIntrRemain = intrCount;
}
if (flags & flagReadIntr)
{
readIntrRemain = intrCount;
}
}
ssize_t write(int /*unused*/, const void *buf, size_t size)
{
// Simulate interruption
if (writeIntrRemain > 0 && writeOffset != 0)
{
writeIntrRemain--;
errno = EINTR;
return -1;
}
// Simulate I/O error halfway through
if ((flags & flagWriteFail) && (writeOffset > deviceSize / 2))
{
errno = EIO;
return -1;
}
auto writeSize =
std::min(deviceSize - writeOffset, static_cast<off_t>(size));
// Simulate short write, but always write at least 1 byte
if ((flags & flagWriteShort) && writeSize > shortCount)
{
writeSize -= shortCount;
}
if (writeSize < 0)
{
return 0;
}
// Only simulate a "write" if flagWriteNoData is false
if (!(flags & flagWriteNoData))
{
std::memcpy(data.data() + writeOffset, buf, writeSize);
}
writeOffset += writeSize;
return writeSize;
}
ssize_t read(int /*unused*/, void *buf, size_t size)
{
// Simulate interruption
if (readIntrRemain > 0 && readOffset != 0)
{
readIntrRemain--;
errno = EINTR;
return -1;
}
// Simulate I/O error halfway through
if ((flags & flagReadFail) && (readOffset > deviceSize / 2))
{
errno = EIO;
return -1;
}
auto readSize =
std::min(deviceSize - readOffset, static_cast<off_t>(size));
// Simulate short read, but always read at least 1 byte
if ((flags & flagReadShort) && readSize > shortCount)
{
readSize -= shortCount;
}
if (readSize < 0)
{
return 0;
}
// Only simulate a "read" if flagReadNoData is false
if (!(flags & flagReadNoData))
{
std::memcpy(buf, data.data() + readOffset, readSize);
}
readOffset += readSize;
return readSize;
}
static constexpr uint32_t flagWriteFail = 1 << 0; // fail write with EIO
static constexpr uint32_t flagWriteIntr = 1 << 1; // fail write with EINTR
static constexpr uint32_t flagWriteShort = 1 << 2; // partial write
static constexpr uint32_t flagWriteNoData = 1 << 3; // no data write
static constexpr uint32_t flagReadFail = 1 << 4; // fail read with EIO
static constexpr uint32_t flagReadIntr = 1 << 5; // fail read with EINTR
static constexpr uint32_t flagReadShort = 1 << 6; // partial read
static constexpr uint32_t flagReadNoData = 1 << 7; // no data read
static constexpr uint32_t flagCloseFail = 1 << 8; // fail close
static constexpr auto deviceSize = 64 * 1024;
static constexpr auto eraseSize = 1024;
static constexpr auto intrCount = 3; // Number of times to fail with EINTR
static constexpr auto shortCount =
7; // Amount to decrease read/write size by
mtd_info_t mtd = {};
off_t writeOffset = 0, readOffset = 0;
std::vector<uint8_t> data;
uint32_t flags;
int readIntrRemain = 0, writeIntrRemain = 0;
};
TEST_P(FlashCopyFakeTest, fakeTest)
{
FakeMtd file(GetParam().first);
std::vector<uint8_t> data(FakeMtd::deviceSize);
std::mt19937 gen(0);
std::generate(data.begin(), data.end(), gen);
EXPECT_CALL(sys, open(StrEq(TEST_PATH), _)).WillOnce(Return(TEST_FD));
EXPECT_CALL(sys, ioctl(TEST_FD, MEMGETINFO, NotNull()))
.WillOnce(DoAll(SetArg2ToMtdPointer(file.mtd), Return(0)));
EXPECT_CALL(
sys, ioctl(TEST_FD, MEMERASE,
MatcherCast<void*>(MatcherCast<erase_info_t*>(
AllOf(Field(&erase_info_t::start, 0),
Field(&erase_info_t::length, file.deviceSize))))))
.WillOnce(Return(0));
EXPECT_CALL(sys, write(TEST_FD, NotNull(), _))
.WillRepeatedly(Invoke(&file, &FakeMtd::write));
EXPECT_CALL(sys, read(TEST_FD, NotNull(), _))
.WillRepeatedly(Invoke(&file, &FakeMtd::read));
// Mock sys->close call, allow error injection with flagCloseFail
if (file.flags & FakeMtd::flagCloseFail)
{
EXPECT_CALL(sys, close(TEST_FD)).WillOnce(Return(-1));
}
else
{
EXPECT_CALL(sys, close(TEST_FD)).WillOnce(Return(0));
}
// This conditional checks whether we expect flashCopy to succeed
if (GetParam().second)
{
google::hoth::internal::mtdImpl.flashCopy(&sys, data, TEST_PATH);
EXPECT_THAT(file.data, ContainerEq(data));
}
else
{
EXPECT_THROW(
google::hoth::internal::mtdImpl.flashCopy(&sys, data, TEST_PATH),
std::exception);
}
}
const std::vector<std::pair<uint32_t, bool>> fakeTestParams{
// Params : {flags, expectation that flashCopy will succeed}
// Passing case without any flags set
{0, true},
// Setting flags for sys->close mock
{FakeMtd::flagCloseFail, false},
// Setting flags for sys->write mock
{FakeMtd::flagWriteIntr, true},
{FakeMtd::flagWriteShort, true},
{FakeMtd::flagWriteIntr | FakeMtd::flagWriteShort, true},
{FakeMtd::flagWriteFail, false},
{FakeMtd::flagWriteNoData, false},
{FakeMtd::flagWriteIntr | FakeMtd::flagWriteShort | FakeMtd::flagWriteFail |
FakeMtd::flagWriteNoData,
false},
// Setting flags for sys->read mock
{FakeMtd::flagReadIntr, true},
{FakeMtd::flagReadShort, true},
{FakeMtd::flagReadIntr | FakeMtd::flagReadShort, true},
{FakeMtd::flagReadFail, false},
{FakeMtd::flagReadNoData, false},
{FakeMtd::flagReadIntr | FakeMtd::flagReadShort | FakeMtd::flagReadFail |
FakeMtd::flagReadNoData,
false},
// Corner case where we do not write or read data
{FakeMtd::flagWriteNoData | FakeMtd::flagReadNoData, false}};
INSTANTIATE_TEST_CASE_P(WithFlags, FlashCopyFakeTest,
::testing::ValuesIn(fakeTestParams));