summaryrefslogtreecommitdiff
path: root/Source/UnitTests/Common/FileUtilTest.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Source/UnitTests/Common/FileUtilTest.cpp')
-rw-r--r--Source/UnitTests/Common/FileUtilTest.cpp265
1 files changed, 265 insertions, 0 deletions
diff --git a/Source/UnitTests/Common/FileUtilTest.cpp b/Source/UnitTests/Common/FileUtilTest.cpp
index 6b3f1168d2..bfdc9128f6 100644
--- a/Source/UnitTests/Common/FileUtilTest.cpp
+++ b/Source/UnitTests/Common/FileUtilTest.cpp
@@ -1,9 +1,15 @@
// Copyright 2020 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <algorithm>
#include <array>
+#include <latch>
+#include <thread>
+
#include <gtest/gtest.h>
+#include "Common/BitUtils.h"
+#include "Common/DirectIOFile.h"
#include "Common/FileUtil.h"
class FileUtilTest : public testing::Test
@@ -147,3 +153,262 @@ TEST_F(FileUtilTest, CreateFullPath)
EXPECT_FALSE(File::CreateFullPath(p3file + "/"));
EXPECT_TRUE(File::IsFile(p3file));
}
+
+TEST_F(FileUtilTest, DirectIOFile)
+{
+ static constexpr std::array<u8, 3> u8_test_data = {42, 7, 99};
+ static constexpr std::array<u16, 3> u16_test_data = {0xdead, 0xbeef, 0xf00d};
+
+ static constexpr int u16_data_offset = 73;
+
+ File::DirectIOFile file;
+ EXPECT_FALSE(file.IsOpen());
+
+ // Read mode fails with a non-existing file.
+ EXPECT_FALSE(file.Open(m_file_path, File::AccessMode::Read));
+
+ std::array<u8, u8_test_data.size()> u8_buffer = {};
+
+ // Everything fails when a file isn't open.
+ EXPECT_FALSE(file.Write(u8_buffer));
+ EXPECT_FALSE(file.Read(u8_buffer));
+ EXPECT_FALSE(file.Flush());
+ EXPECT_FALSE(file.Seek(12, File::SeekOrigin::Begin));
+ EXPECT_EQ(file.Tell(), 0);
+ EXPECT_EQ(file.GetSize(), 0);
+
+ // Fail to open non-existing file.
+ EXPECT_FALSE(file.Open(m_file_path, File::AccessMode::ReadAndWrite));
+ EXPECT_FALSE(file.Open(m_file_path, File::AccessMode::Read, File::OpenMode::Existing));
+ EXPECT_FALSE(file.Open(m_file_path, File::AccessMode::Write, File::OpenMode::Existing));
+
+ // Read mode can create files
+ EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Read, File::OpenMode::Create));
+ EXPECT_TRUE(file.Close());
+
+ // Open a file for writing.
+ EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Write, File::OpenMode::Always));
+ EXPECT_TRUE(file.Flush());
+ EXPECT_TRUE(file.IsOpen());
+
+ // Note: Double Open() currently ASSERTs. It's not obvious if that should succeed or fail.
+
+ EXPECT_TRUE(file.Close());
+ EXPECT_FALSE(file.Open(m_file_path, File::AccessMode::Write, File::OpenMode::Create));
+
+ EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Write, File::OpenMode::Existing));
+ EXPECT_TRUE(file.Close());
+
+ // Rename, Resize, and Delete fail with a closed handle.
+ EXPECT_FALSE(Rename(file, m_file_path, "fail"));
+ EXPECT_FALSE(Resize(file, 72));
+ EXPECT_FALSE(Delete(file, m_file_path));
+ EXPECT_TRUE(File::Exists(m_file_path));
+
+ EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Write));
+
+ EXPECT_TRUE(file.Write(u8_buffer.data(), 0)); // write of 0 succeeds.
+ EXPECT_FALSE(file.Read(u8_buffer.data(), 0)); // read of 0 (in write mode) fails.
+
+ // Resize through handle works.
+ EXPECT_TRUE(Resize(file, 1));
+ EXPECT_EQ(file.GetSize(), 1);
+ file.Close();
+
+ // Write truncates by default.
+ EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Write));
+ EXPECT_EQ(file.GetSize(), 0);
+
+ EXPECT_TRUE(file.Write(u8_test_data));
+ EXPECT_EQ(file.GetSize(), u8_test_data.size()); // size changed
+ EXPECT_EQ(file.Tell(), u8_test_data.size()); // file position changed
+
+ // Relative seek works.
+ EXPECT_TRUE(file.Seek(0, File::SeekOrigin::End));
+ EXPECT_EQ(file.Tell(), file.GetSize());
+ EXPECT_TRUE(file.Seek(-int(u8_test_data.size()), File::SeekOrigin::Current));
+ EXPECT_EQ(file.Tell(), 0);
+
+ // Read while in "write mode" fails
+ EXPECT_FALSE(file.Read(u8_buffer));
+ EXPECT_EQ(file.Tell(), 0);
+
+ // Seeking past the end works.
+ EXPECT_TRUE(file.Seek(u16_data_offset, File::SeekOrigin::Begin));
+ EXPECT_EQ(file.Tell(), u16_data_offset);
+ EXPECT_EQ(file.GetSize(), u8_test_data.size()); // no change in size
+
+ static constexpr u64 final_file_size = u16_data_offset + sizeof(u16_test_data);
+
+ // Size changes after write.
+ EXPECT_TRUE(file.Write(Common::AsU8Span(u16_test_data)));
+ EXPECT_EQ(file.GetSize(), final_file_size);
+
+ // Seek before begin fails.
+ EXPECT_FALSE(file.Seek(-1, File::SeekOrigin::Begin));
+ EXPECT_EQ(file.Tell(), file.GetSize()); // unchanged position
+
+ EXPECT_TRUE(file.Close());
+ EXPECT_FALSE(file.IsOpen());
+
+ EXPECT_EQ(file.Tell(), 0);
+ EXPECT_EQ(file.GetSize(), 0);
+
+ EXPECT_FALSE(file.Close()); // Double close fails.
+
+ // Open file for reading.
+ EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Read, File::OpenMode::Always));
+ EXPECT_TRUE(file.Close());
+ EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Read));
+
+ static constexpr int big_offset = 999;
+
+ // We can seek beyond the end.
+ EXPECT_TRUE(file.Seek(big_offset, File::SeekOrigin::End));
+ EXPECT_EQ(file.Tell(), file.GetSize() + big_offset);
+
+ // Resize through handle fails when in read mode.
+ EXPECT_FALSE(Resize(file, big_offset));
+ EXPECT_EQ(file.GetSize(), final_file_size);
+
+ constexpr size_t thread_count = 4;
+
+ File::DirectIOFile closed_file;
+
+ std::latch do_reads{1};
+
+ EXPECT_TRUE(file.Seek(u16_data_offset, File::SeekOrigin::Begin));
+
+ // Concurrent access with copied handles works.
+ std::vector<std::thread> threads(thread_count);
+ for (auto& t : threads)
+ {
+ t = std::thread{[&do_reads, file, closed_file]() mutable {
+ EXPECT_FALSE(closed_file.IsOpen()); // a copied closed file is still closed
+
+ EXPECT_EQ(file.Tell(), u16_data_offset); // current position is copied
+
+ std::array<u8, 1> one_byte{};
+
+ constexpr u64 small_offset = 3;
+
+ do_reads.wait();
+
+ EXPECT_TRUE(file.Seek(small_offset, File::SeekOrigin::Begin));
+ EXPECT_EQ(file.Tell(), small_offset);
+
+ // writes fail in read mode.
+ EXPECT_FALSE(file.Write(u8_test_data));
+ EXPECT_FALSE(file.OffsetWrite(0, u8_test_data.data(), 0));
+ EXPECT_EQ(file.Tell(), small_offset);
+
+ EXPECT_TRUE(file.Read(one_byte.data(), 0)); // read of zero succeeds.
+ EXPECT_EQ(file.Tell(), small_offset); // unchanged position
+
+ // Reading at an explicit offset doesn't change the position
+ EXPECT_TRUE(file.OffsetRead(u8_test_data.size() - 1, one_byte));
+ EXPECT_EQ(file.Tell(), small_offset);
+ EXPECT_EQ(one_byte[0], u8_test_data.back());
+
+ EXPECT_EQ(file.GetSize(), final_file_size);
+ EXPECT_TRUE(file.Seek(-int(sizeof(u16_test_data)), File::SeekOrigin::End));
+
+ // We can't read beyond the end of the file.
+ EXPECT_FALSE(file.OffsetRead(big_offset, one_byte));
+ // A read of zero beyond the end works.
+ EXPECT_TRUE(file.OffsetRead(big_offset, one_byte.data(), 0));
+
+ // Reading the previously written data works.
+ std::array<u16, u16_test_data.size()> u16_buffer = {};
+ EXPECT_TRUE(file.Read(Common::AsWritableU8Span(u16_buffer)));
+ EXPECT_TRUE(std::ranges::equal(u16_buffer, u16_test_data));
+
+ // We can't read at the end of the file.
+ EXPECT_FALSE(file.Read(one_byte));
+ EXPECT_EQ(file.Tell(), file.GetSize()); // The position is unchanged.
+ }};
+ }
+
+ // We may close the file on our end before the other threads use it.
+ file.Close();
+
+ // ReadAndWrite does not truncate existing files.
+ EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::ReadAndWrite));
+ EXPECT_EQ(file.GetSize(), final_file_size);
+ file.Close();
+
+ // Write doesn't truncate when the mode is changed.
+ EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Write, File::OpenMode::Existing));
+ EXPECT_EQ(file.GetSize(), final_file_size);
+
+ // Open a new handle to the same file.
+ File::DirectIOFile second_handle(m_file_path, File::AccessMode::Write, File::OpenMode::Always);
+ EXPECT_TRUE(second_handle.IsOpen());
+
+ const std::string destination_path_1 = m_file_path + ".dest_1";
+
+ // Rename through handle works when opened elsewhere.
+ EXPECT_TRUE(Rename(file, m_file_path, destination_path_1));
+ EXPECT_FALSE(File::Exists(m_file_path));
+ EXPECT_TRUE(File::Exists(destination_path_1));
+
+ const std::string destination_path_2 = m_file_path + ".dest_2";
+
+ // Note: Windows fails the next `Rename` if this file is kept open.
+ // I don't know if there is a nice way to make that work.
+ {
+ File::DirectIOFile another_file{destination_path_2, File::AccessMode::Write};
+ EXPECT_TRUE(another_file.IsOpen());
+ }
+
+ // Rename overwrites existing files.
+ EXPECT_TRUE(Rename(file, destination_path_1, destination_path_2));
+ EXPECT_FALSE(File::Exists(destination_path_1));
+ EXPECT_EQ(File::GetSize(destination_path_2), final_file_size);
+
+ const std::string destination_path_3 = m_file_path + ".dest_3";
+
+ // Truncate fails in Read mode.
+ File::Copy(destination_path_2, destination_path_3);
+ EXPECT_EQ(File::GetSize(destination_path_3), final_file_size);
+ {
+ File::DirectIOFile tmp{destination_path_3, File::AccessMode::Read, File::OpenMode::Truncate};
+ EXPECT_FALSE(tmp.IsOpen());
+ EXPECT_EQ(File::GetSize(destination_path_3), final_file_size);
+ }
+
+ // Truncate works with ReadAndWrite.
+ EXPECT_EQ(File::GetSize(destination_path_3), final_file_size);
+ {
+ File::DirectIOFile tmp{destination_path_3, File::AccessMode::ReadAndWrite,
+ File::OpenMode::Truncate};
+ EXPECT_EQ(tmp.GetSize(), 0);
+ Delete(tmp, destination_path_3);
+ }
+
+ // Truncate works with Write.
+ File::Copy(destination_path_2, destination_path_3);
+ EXPECT_EQ(File::GetSize(destination_path_3), final_file_size);
+ {
+ File::DirectIOFile tmp{destination_path_3, File::AccessMode::Write, File::OpenMode::Truncate};
+ EXPECT_EQ(tmp.GetSize(), 0);
+ Delete(tmp, destination_path_3);
+ }
+
+ // Delete through handle works.
+ EXPECT_TRUE(Delete(file, destination_path_2));
+ EXPECT_FALSE(File::Exists(destination_path_2));
+
+ // The threads can read even after deleting everything.
+ do_reads.count_down();
+ std::ranges::for_each(threads, &std::thread::join);
+
+ file.Close();
+ EXPECT_TRUE(file.Open(destination_path_1, File::AccessMode::Read, File::OpenMode::Always));
+
+ // Required on Windows for the below to succeed.
+ second_handle.Close();
+
+ file.Close();
+ EXPECT_TRUE(file.Open(destination_path_2, File::AccessMode::Write, File::OpenMode::Always));
+}