Files
hrynco-kswitcher/tests/files_text_document.cpp
T

163 lines
5.9 KiB
C++

#include "files_text_document.h"
#include <objbase.h>
#include <winioctl.h>
#include <fstream>
#include <iostream>
#include <stdexcept>
namespace fs = std::filesystem;
using ks::files::TextDocument;
namespace
{
void check(bool condition, const char *message)
{
if (!condition) throw std::runtime_error(message);
}
struct Fixture
{
fs::path directory;
Fixture()
{
GUID id{};
wchar_t name[40]{};
check(SUCCEEDED(CoCreateGuid(&id)) && StringFromGUID2(id, name, 40), "Create fixture ID");
directory = fs::temp_directory_path() / (std::wstring(L"ks-files-text-") + name);
fs::create_directory(directory);
}
~Fixture()
{
std::error_code error;
fs::remove_all(directory, error);
}
};
void writeFile(const fs::path &path, const std::string &bytes)
{
std::ofstream output(path, std::ios::binary);
output.write(bytes.data(), bytes.size());
check(static_cast<bool>(output), "Write fixture");
}
std::string readFile(const fs::path &path)
{
std::ifstream input(path, std::ios::binary);
check(static_cast<bool>(input), "Read fixture");
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
void editsAcrossPages(const fs::path &file)
{
const std::string first = std::string(TextDocument::pageBytes - 2, 'a') + "\r\n";
const std::string middle = std::string(TextDocument::pageBytes - 2, 'b') + "\r\n";
const std::string last = "last\r\n";
const std::string bom = "\xef\xbb\xbf";
writeFile(file, bom + first + middle + last);
TextDocument document(file);
check(document.pages() == 3, "Expected three pages");
check(document.readPage(1) == middle, "Read middle page");
document.stage(2, "last changed\r\n");
check(!document.modifiedWith(2, last), "Restoring the only modified page leaves no changes");
check(document.modifiedWith(0, first), "An original page must not hide changes to other pages");
document.stage(0, "first changed\n");
check(document.readPage(2) == "last changed\r\n", "Read staged page");
check(document.save(), "Replace and reopen");
check(!document.modified(), "Save clears staged page edits");
check(readFile(file) == bom + "first changed\n" + middle + "last changed\r\n",
"Save must preserve untouched bytes and the BOM");
document.stage(0, "saved again\n");
check(document.save(), "Save again through the same document");
}
void unicodeBoundaries(const fs::path &file)
{
const auto chunk = TextDocument::pageBytes;
const std::string emoji = "\xf0\x9f\x98\x80";
writeFile(file, std::string(chunk - 1, 'a') + emoji + "tail");
{
TextDocument document(file, false);
check(document.boundary(1) == chunk + 3, "UTF-8 split within a code point");
check(document.readPage(0).ends_with(emoji) && document.readPage(1) == "tail", "Read split UTF-8");
}
for (const bool little : {true, false})
{
const std::string bom = little ? "\xff\xfe" : "\xfe\xff";
const std::string letter(little ? "a\0" : "\0a", 2);
const std::string pair(little ? "\x3d\xd8\x00\xde" : "\xd8\x3d\xde\x00", 4);
std::string first;
for (size_t n = 0; n < chunk / 2 - 1; ++n) first += letter;
first += pair;
writeFile(file, bom + first + letter);
TextDocument document(file);
check(document.readPage(0).ends_with(emoji) && document.readPage(1) == "a", "Read split UTF-16");
document.stage(1, emoji);
check(document.save() && readFile(file) == bom + first + pair, "Preserve UTF-16 byte order");
}
}
void rejectExternalReplacement(const fs::path &file)
{
writeFile(file, "original");
TextDocument document(file);
document.stage(0, "editor changes");
auto later = file;
later += L".later";
auto original = file;
original += L".original";
writeFile(later, "external changes");
check(MoveFileExW(file.c_str(), original.c_str(), 0) && MoveFileExW(later.c_str(), file.c_str(), 0),
"Replace file externally");
bool rejected{};
try { document.save(); }
catch (const std::exception &) { rejected = true; }
check(rejected && document.modified() && readFile(file) == "external changes",
"Preserve external file and staged changes after conflict");
}
void sparseFile(const fs::path &file)
{
constexpr uint64_t offset = 5ULL << 30;
const auto handle = CreateFileW(file.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
check(handle != INVALID_HANDLE_VALUE, "Create sparse fixture");
DWORD count{};
if (!DeviceIoControl(handle, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &count, nullptr))
{
CloseHandle(handle);
std::cout << "Sparse fixture skipped on this file system.\n";
return;
}
LARGE_INTEGER position{};
position.QuadPart = offset;
const bool written = SetFilePointerEx(handle, position, nullptr, FILE_BEGIN) &&
WriteFile(handle, "last", 4, &count, nullptr) && count == 4;
CloseHandle(handle);
check(written, "Write beyond the 32-bit range");
TextDocument document(file);
const auto index = document.pages() - 1;
check(document.size() == offset + 4 && document.boundary(index) == offset, "Use 64-bit offsets");
check(document.readPage(index) == "last", "Read past 4 GiB");
document.stage(index, "edited");
check(document.readPage(index) == "edited", "Stage an edit past 4 GiB");
}
}
int main()
{
try
{
const Fixture fixture;
editsAcrossPages(fixture.directory / L"pages.txt");
unicodeBoundaries(fixture.directory / L"unicode.txt");
rejectExternalReplacement(fixture.directory / L"external.txt");
sparseFile(fixture.directory / L"5gib.log");
std::cout << "Files text document regressions passed.\n";
}
catch (const std::exception &error)
{
std::cerr << error.what() << '\n';
return 1;
}
}