| // 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)); |