diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /libs/usvfs/test/usvfs_test_runner/usvfs_test | |
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem,
Proton/umu-run integration, and Flatpak packaging.
Key features:
- FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak)
- Proton/GE-Proton/umu-run launcher with env var forwarding
- Flatpak support (sandbox-aware VFS, NXM handler, umu-run)
- Wine prefix management UI
- Case-insensitive path resolution for Linux filesystems
- QSettings-safe INI handling (avoids Bethesda INI corruption)
- Portable instance support with auto-generated launcher scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/usvfs/test/usvfs_test_runner/usvfs_test')
5 files changed, 1780 insertions, 0 deletions
diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.cpp b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.cpp new file mode 100644 index 0000000..c5fb3a1 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.cpp @@ -0,0 +1,402 @@ +#include "usvfs_basic_test.h" + +const char* usvfs_basic_test::scenario_name() +{ + return SCENARIO_NAME; +} + +bool usvfs_basic_test::scenario_run() +{ + // Note: For regression purposes we don't really need to verify the results of most + // our operations as the usvfs_test_base postmortem_check will verify the final state. + // We still also verify the results here because: + // A. In some cases a later step may change the results. + // B. It is easier to understand and maintain the test when the important checks are + // together with + // their related operations. + // C. For open issues the verifications here serve as a "documentation" of the issue + // (i.e. not having + // proper copy_on_write, etc.). + + // Cases not covered by the test: + // - When a "temporary" virtualized directory is created (meaning the directory is + // created only to "shadow" + // a real directory) and is then emptied. This needs to be tested within the same + // prcoess and across different processes. + + ops_list(LR"(.)", true, true); + + // test proper path creation under overwrite when a virtualized folder is written to: + + verify_source_existance(LR"(overwrite\mfolder1)", false); + verify_source_existance(LR"(overwrite\mfolder2)", false); + verify_source_existance(LR"(overwrite\mfolder3)", false); + verify_source_existance(LR"(overwrite\mfolder4)", false); + + ops_overwrite(LR"(mfolder1\newfolder1\newfile1.txt)", + R"(newfile1.txt nonrecursive overwrite subfolder)", false); + verify_source_existance(LR"(overwrite\mfolder1\newfolder1\newfile1.txt)"); + ops_overwrite(LR"(mfolder1\newfile1.txt)", R"(newfile1.txt nonrecursive overwrite)", + false); + ops_read(LR"(mfolder1\newfile1.txt)"); + + ops_overwrite(LR"(mfolder2\newfile2.txt)", R"(newfile2.txt recursive overwrite)", + true); + ops_read(LR"(mfolder2\newfile2.txt)"); + verify_source_contents(LR"(overwrite\mfolder2\newfile2.txt)", + R"(newfile2.txt recursive overwrite)"); + ops_overwrite(LR"(mfolder2\newfile2.txt\fail)", + R"(newfile2.txt is a file so folder creation should fail)", true, + false); + verify_source_contents(LR"(overwrite\mfolder2\newfile2.txt)", + R"(newfile2.txt recursive overwrite)"); + ops_overwrite(LR"(mfolder2\mfile.txt\fail)", + R"(mfile.txt is a file so folder creation should fail)", true, false); + verify_source_existance(LR"(overwrite\mfolder2\mfile.txt)", false); + + ops_overwrite(LR"(mfolder3\newfolder3\newfile3.txt)", + R"(newfile3.txt recursive overwrite)", true); + ops_read(LR"(mfolder3\newfolder3\newfile3.txt)"); + verify_source_contents(LR"(overwrite\mfolder3\newfolder3\newfile3.txt)", + R"(newfile3.txt recursive overwrite)"); + // repeat mfolder3\newfolder3 test as that folder now exists in overwrite and that + // changes things + ops_overwrite(LR"(mfolder3\newfolder3\newfile3e.txt)", + R"(newfile3e.txt recursive overwrite)", true); + ops_read(LR"(mfolder3\newfolder3\newfile3e.txt)"); + verify_source_contents(LR"(overwrite\mfolder3\newfolder3\newfile3e.txt)", + R"(newfile3e.txt recursive overwrite)"); + + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4.txt)", + R"(newfile4.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4.txt)", + R"(newfile4.txt recursive overwrite)"); + // repeat mfolder4\newfolder4\d\e\e\p test as that folder now exists in overwrite and + // that changes things + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4e.txt)", + R"(newfile4e.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4e.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4e.txt)", + R"(newfile4e.txt recursive overwrite)"); + // and finally verify also non-recursive works + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)", + R"(newfile4enr.txt nonrecursive overwrite)", false); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)", + R"(newfile4enr.txt nonrecursive overwrite)"); + // finally check an intermediate folder: + ops_overwrite(LR"(mfolder4\newfolder4\d\e\epnewfile4r.txt)", + R"(epnewfile4r.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\epnewfile4r.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\epnewfile4r.txt)", + R"(epnewfile4r.txt recursive overwrite)"); + ops_overwrite(LR"(mfolder4\newfolder4\d\e\epnewfile4.txt)", + R"(epnewfile4.txt nonrecursive overwrite)", false); + ops_read(LR"(mfolder4\newfolder4\d\e\epnewfile4.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\epnewfile4.txt)", + R"(epnewfile4.txt nonrecursive overwrite)"); + + verify_mount_existance(LR"(rfolder\rcopyme4.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\newfolder4p)", false); + ops_copy(LR"(rfolder\rcopyme4.txt)", LR"(mfolder4\newfolder4p\rcopyme4.txt)", true); + verify_source_existance(LR"(overwrite\mfolder4\newfolder4p\rcopyme4.txt)", true); + verify_source_existance(LR"(mod4\mfolder4\mfile.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\mfile.txt)", false); + ops_copy(LR"(rfolder\rcopyme4.txt)", LR"(mfolder4\rcopyme4.txt)", false); + verify_source_existance(LR"(overwrite\mfolder4\rcopyme4.txt)"); + + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4.txt)", + R"(newfile4.txt recursive overwrite)"); + // repeat mfolder4\newfolder4\d\e\e\p test as that folder now exists in overwrite and + // that changes things + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4e.txt)", + R"(newfile4e.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4e.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4e.txt)", + R"(newfile4e.txt recursive overwrite)"); + // and finally verify also non-recursive works + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)", + R"(newfile4enr.txt nonrecursive overwrite)", false); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)", + R"(newfile4enr.txt nonrecursive overwrite)"); + // finally check an intermediate folder: + ops_overwrite(LR"(mfolder4\newfolder4\d\e\epnewfile4r.txt)", + R"(epnewfile4r.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\epnewfile4r.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\epnewfile4r.txt)", + R"(epnewfile4r.txt recursive overwrite)"); + ops_overwrite(LR"(mfolder4\newfolder4\d\e\epnewfile4.txt)", + R"(epnewfile4.txt nonrecursive overwrite)", false); + ops_read(LR"(mfolder4\newfolder4\d\e\epnewfile4.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\epnewfile4.txt)", + R"(epnewfile4.txt nonrecursive overwrite)"); + + // test copy on write/delete against source "mod": + + ops_touch(LR"(root0.txt)"); + verify_source_existance(LR"(overwrite\root0.txt)", false); + ops_touch(LR"(root1.txt)"); + verify_source_existance(LR"(overwrite\root1.txt)", false); + ops_touch(LR"(root2.txt)"); + verify_source_existance(LR"(overwrite\root1.txt)", false); + ops_touch(LR"(mod1.txt)"); + verify_source_existance(LR"(overwrite\mod1.txt)", false); + ops_touch(LR"(mfolder1\mfile.txt)"); + verify_source_existance(LR"(overwrite\mfolder1\mfile.txt)", false); + + ops_touch(LR"(root0w.txt)", true); + verify_source_existance(LR"(overwrite\root0w.txt)", false); + ops_touch(LR"(root1w.txt)", true); + verify_source_existance(LR"(overwrite\root1w.txt)", false); + ops_touch(LR"(root2w.txt)", true); + verify_source_existance(LR"(overwrite\root1w.txt)", false); + ops_touch(LR"(mod1w.txt)", true); + verify_source_existance(LR"(overwrite\mod1w.txt)", false); + ops_touch(LR"(mfolder1\mfilew.txt)", true); + verify_source_existance(LR"(overwrite\mfolder1\mfilew.txt)", false); + + { + const auto& old_contents = source_contents(LR"(mod4\mfolder4\mfileoverwrite.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfileoverwrite.txt)", + old_contents.c_str()); + ops_overwrite(LR"(mfolder4\mfileoverwrite.txt)", + R"(mfolder4\mfileoverwrite.txt overwrite)", false); + ops_read(LR"(mfolder4\mfileoverwrite.txt)"); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(mod4\mfolder4\mfileoverwrite.txt)", + old_contents.c_str()); + verify_source_contents(LR"(overwrite\mfolder4\mfileoverwrite.txt)", + R"(mfolder4\mfileoverwrite.txt overwrite)"); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfileoverwrite.txt)", + R"(mfolder4\mfileoverwrite.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfileoverwrite.txt)", false); + } + } + + { + const auto& old_contents = source_contents(LR"(mod4\mfolder4\mfilerewrite.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfilerewrite.txt)", old_contents.c_str()); + ops_rewrite(LR"(mfolder4\mfilerewrite.txt)", + R"(mfolder4\mfilerewrite.txt rewrite)"); + ops_read(LR"(mfolder4\mfilerewrite.txt)"); + if (auto copy_on_readwrite_implemented = false) { + verify_source_contents(LR"(mod4\mfolder4\mfilerewrite.txt)", + old_contents.c_str()); + verify_source_contents(LR"(overwrite\mfolder4\mfilerewrite.txt)", + R"(mfolder4\mfilerewrite.txt rewrite)"); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfilerewrite.txt)", + R"(mfolder4\mfilerewrite.txt rewrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfilerewrite.txt)", false); + } + } + + { + const auto& old_contents = source_contents(LR"(mod4\mfolder4\mfilemoveover.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfilemoveover.txt)", old_contents.c_str()); + ops_overwrite(LR"(mfolder4\temp_mfilemoveover.txt)", + R"(mfolder4\mfilemoveover.txt overwrite)", false); + verify_source_contents(LR"(overwrite\mfolder4\temp_mfilemoveover.txt)", + R"(mfolder4\mfilemoveover.txt overwrite)"); + ops_rename(LR"(mfolder4\temp_mfilemoveover.txt)", LR"(mfolder4\mfilemoveover.txt)", + true); + ops_read(LR"(mfolder4\mfilemoveover.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\temp_mfilemoveover.txt)", false); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(mod4\mfolder4\mfilemoveover.txt)", + old_contents.c_str()); + verify_source_contents(LR"(overwrite\mfolder4\mfilemoveover.txt)", + R"(mfolder4\mfilemoveover.txt overwrite)"); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfilemoveover.txt)", + R"(mfolder4\mfilemoveover.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfilemoveover.txt)", false); + } + } + + { + const auto& old_contents = + source_contents(LR"(mod4\mfolder4\mfiledeletewrite.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite.txt)", + old_contents.c_str()); + ops_deleteoverwrite(LR"(mfolder4\mfiledeletewrite.txt)", + R"(mfolder4\mfiledeletewrite.txt overwrite)", false); + ops_read(LR"(mfolder4\mfiledeletewrite.txt)"); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(overwrite\mfolder4\mfiledeletewrite.txt)", + R"(mfolder4\mfiledeletewrite.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite.txt)", + old_contents.c_str()); + else + verify_source_existance(LR"(mod4\mfolder4\mfiledeletewrite.txt)", false); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite.txt)", + R"(mfolder4\mfiledeletewrite.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfiledeletewrite.txt)", false); + } + } + + { + const auto& old_contents = + source_contents(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)", + old_contents.c_str()); + ops_delete(LR"(mfolder4\mfiledeletewrite2p.txt)"); + ops_overwrite(LR"(mfolder4\mfiledeletewrite2p.txt)", + R"(mfolder4\mfiledeletewrite2p.txt overwrite)", false); + ops_read(LR"(mfolder4\mfiledeletewrite2p.txt)"); + if (bool protect_virtualized_or_track_deleted_only_in_current_process = true) { + verify_source_contents(LR"(overwrite\mfolder4\mfiledeletewrite2p.txt)", + R"(mfolder4\mfiledeletewrite2p.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)", + old_contents.c_str()); + else + verify_source_existance(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)", false); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)", + R"(mfolder4\mfiledeletewrite2p.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfiledeletewrite2p.txt)", false); + } + } + + { + const auto& old_contents = source_contents(LR"(mod4\mfolder4\mfiledeletemove.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove.txt)", + old_contents.c_str()); + ops_overwrite(LR"(mfolder4\temp_mfiledeletemove.txt)", + R"(mfolder4\mfiledeletemove.txt overwrite)", false); + verify_source_contents(LR"(overwrite\mfolder4\temp_mfiledeletemove.txt)", + R"(mfolder4\mfiledeletemove.txt overwrite)"); + ops_deleterename(LR"(mfolder4\temp_mfiledeletemove.txt)", + LR"(mfolder4\mfiledeletemove.txt)"); + ops_read(LR"(mfolder4\mfiledeletemove.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\temp_mfiledeletemove.txt)", false); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(overwrite\mfolder4\mfiledeletemove.txt)", + R"(mfolder4\mfiledeletemove.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove.txt)", + old_contents.c_str()); + else + verify_source_existance(LR"(mod4\mfolder4\mfiledeletemove.txt)", false); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove.txt)", + R"(mfolder4\mfiledeletemove.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfiledeletemove.txt)", false); + } + } + + { + const auto& old_contents = + source_contents(LR"(mod4\mfolder4\mfiledeletemove2p.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove2p.txt)", + old_contents.c_str()); + ops_delete(LR"(mfolder4\mfiledeletemove2p.txt)"); + ops_overwrite(LR"(mfolder4\temp_mfiledeletemove2p.txt)", + R"(mfolder4\mfiledeletemove2p.txt overwrite)", false); + verify_source_contents(LR"(overwrite\mfolder4\temp_mfiledeletemove2p.txt)", + R"(mfolder4\mfiledeletemove2p.txt overwrite)"); + ops_rename(LR"(mfolder4\temp_mfiledeletemove2p.txt)", + LR"(mfolder4\mfiledeletemove2p.txt)", false); + ops_read(LR"(mfolder4\mfiledeletemove2p.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\temp_mfiledeletemove2p.txt)", false); + if (bool protect_virtualized_or_track_deleted_only_in_current_process = true) { + verify_source_contents(LR"(overwrite\mfolder4\mfiledeletemove2p.txt)", + R"(mfolder4\mfiledeletemove2p.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove2p.txt)", + old_contents.c_str()); + else + verify_source_existance(LR"(mod4\mfolder4\mfiledeletemove2p.txt)", false); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove2p.txt)", + R"(mfolder4\mfiledeletemove2p.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfiledeletemove2p.txt)", false); + } + } + + // test copy on write/delete/move against original mount files: + + { + const auto& old_contents = mount_contents(LR"(rfolder\rfilerewrite.txt)"); + ops_rewrite(LR"(rfolder\rfilerewrite.txt)", R"(rfolder\rfilerewrite.txt rewrite)"); + ops_read(LR"(rfolder\rfilerewrite.txt)"); + if (auto copy_on_readwrite_implemented = false) { + verify_mount_contents(LR"(rfolder\rfilerewrite.txt)", old_contents.c_str()); + verify_source_contents(LR"(overwrite\rfolder\rfilerewrite.txt)", + R"(rfolder\rfilerewrite.txt rewrite)"); + } else { + verify_mount_contents(LR"(rfolder\rfilerewrite.txt)", + R"(rfolder\rfilerewrite.txt rewrite)"); + verify_source_existance(LR"(overwrite\rfolder\rfilerewrite.txt)", false); + } + } + ops_overwrite(LR"(rfolder\rfilerewrite.txt\fail)", + R"(rfilerewrite.txt is a file so folder creation should fail)", true, + false); + verify_mount_existance( + LR"(rfolder\rfilerewrite.txt)"); // verifies its a file and not a directory + if (auto copy_on_readwrite_implemented = false) + verify_source_existance( + LR"(overwrite\rfolder\rfilerewrite.txt)"); // verifies its a file and not a + // directory + else + verify_source_existance(LR"(overwrite\rfolder\rfilerewrite.txt)", false); + ops_overwrite(LR"(rfolder\rfile0.txt\fail)", + R"(rfile0.txt is a file so folder creation should fail)", true, false); + verify_mount_existance( + LR"(rfolder\rfile0.txt)"); // verifies its a file and not a directory + + { + const auto& old_contents = mount_contents(LR"(rfolder\rfiledeletewrite.txt)"); + ops_deleteoverwrite(LR"(rfolder\rfiledeletewrite.txt)", + R"(rfolder\rfiledeletewrite.txt overwrite)", false); + ops_read(LR"(rfolder\rfiledeletewrite.txt)"); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(overwrite\rfolder\rfiledeletewrite.txt)", + R"(rfolder\rfiledeletewrite.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_mount_contents(LR"(rfolder\rfiledeletewrite.txt)", old_contents.c_str()); + else + verify_mount_existance(LR"(rfolder\rfiledeletewrite.txt)", false); + } else { + verify_mount_contents(LR"(rfolder\rfiledeletewrite.txt)", + R"(rfolder\rfiledeletewrite.txt overwrite)"); + verify_source_existance(LR"(overwrite\rfolder\rfiledeletewrite.txt)", false); + } + } + + { + const auto& old_contents = mount_contents(LR"(rfolder\rfiledelete.txt)"); + ops_delete(LR"(rfolder\rfiledelete.txt)"); + ops_read(LR"(rfolder\rfiledelete.txt)", false); + if (auto proper_delete_implemented = false) + verify_mount_contents(LR"(rfolder\rfiledelete.txt)", old_contents.c_str()); + else + verify_mount_existance(LR"(rfolder\rfiledelete.txt)", false); + } + + { + const auto& old_contents = mount_contents(LR"(rfolder\rfileoldname.txt)"); + ops_rename(LR"(rfolder\rfileoldname.txt)", LR"(rfolder\rfilenewname.txt)", false, + false); + ops_read(LR"(rfolder\rfileoldname.txt)", false); + ops_read(LR"(rfolder\rfilenewname.txt)"); + verify_source_contents(LR"(overwrite\rfolder\rfilenewname.txt)", + old_contents.c_str()); + verify_mount_existance(LR"(rfolder\rfilenewname.txt)", false); + if (auto copy_on_move_implemented = false) + verify_mount_contents(LR"(rfolder\rfileoldname.txt)", old_contents.c_str()); + else + verify_mount_existance(LR"(rfolder\rfileoldname.txt)", false); + } + + ops_list(LR"(.)", true, true); + + return true; +} diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.h b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.h new file mode 100644 index 0000000..bec81f2 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.h @@ -0,0 +1,14 @@ +#pragma once + +#include "usvfs_test_base.h" + +class usvfs_basic_test : public usvfs_test_base +{ +public: + static constexpr auto SCENARIO_NAME = "basic"; + + usvfs_basic_test(const usvfs_test_options& options) : usvfs_test_base(options) {} + + virtual const char* scenario_name(); + virtual bool scenario_run(); +}; diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test.cpp b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test.cpp new file mode 100644 index 0000000..e6e6bb1 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test.cpp @@ -0,0 +1,242 @@ + +#include <iostream> +#include <memory> + +#include <boost/filesystem.hpp> +#include <boost/type_traits.hpp> +#include <winapi.h> + +#include "usvfs_basic_test.h" +#include <stringcast.h> + +void print_usage(const std::wstring& exe_name, const std::wstring& test_name) +{ + using namespace std; + wcerr << "usage: " << exe_name << " [<options>] <scenario>[:<label>]" << endl; + wcerr << "available options:" << endl; + wcerr << " -ops32 : force 32bit file opertations process (default is same " + "bitness)." + << endl; + wcerr << " -ops64 : force 64bit file opertations process (default is same " + "bitness)." + << endl; + wcerr << " -opsexe <file> : full path to file operations executable (overrides " + "-ops32/64)." + << endl; + wcerr << " -opsarg <arg> : adds argument to the start of all file operations." + << endl; + wcerr << " -fixture <dir> : fixture dir (default is test\\fixtures\\" << test_name + << "\\<scenario>)." << endl; + wcerr << " -mapping <file> : mapping file (default is <fixture " + "dir>\\vfs_mappings.txt)." + << endl; + wcerr << " -temp <dir> : temp dir (default is test\\temp\\" << test_name + << "\\<scenario>_<label>)." << endl; + wcerr << " -mount <dir> : mount dir (default is <temp dir>\\mount)." << endl; + wcerr << " -source <dir> : source dir (default is <temp dir>\\source)." << endl; + wcerr << " -out <file> : output file (default is <temp " + "dir>\\<scenario>_<label>.log)." + << endl; + wcerr << " -usvfslog <file> : output file (default is <temp " + "dir>\\<scenario>_<label>_usvfs.log)." + << endl; + wcerr << " -forcetemprecursivedelete : decimate temp dir even if doesn't look like a " + "temp dir." + << endl; + wcerr << endl; + wcerr << "note: if <label> is ommited the current platform (x86/x64) is used." + << endl; + wcerr << "note: mount and source dirs should not exist or be empty directories. if " + "either of them" + << endl; + wcerr << "is not given, the temp dir is first recusrively deleted. to protect " + "against accidents a" + << endl; + wcerr << "heuristic is used to verify the temp dir only has entires which this test " + "creates." + << endl; + wcerr << "-forcetemprecursivedelete can be used to circumvent this heuristic." + << endl; +} + +usvfs_test_base* find_scenario(const std::string& scenario, + const usvfs_test_options& options) +{ + if (scenario == usvfs_basic_test::SCENARIO_NAME) + return new usvfs_basic_test(options); + else + return nullptr; +} + +bool verify_args_exist(const wchar_t* flag, int params, int index, int count) +{ + if (index + params >= count) { + std::wcerr << flag << L" requires " << params << L" arguments" << std::endl; + return false; + } else + return true; +} + +bool verify_file(const test::path& file) +{ + bool is_dir = false; + if (!winapi::ex::wide::fileExists(file.c_str(), &is_dir) || is_dir) { + std::wcerr << L"File does not exist: " << file << std::endl; + return false; + } else + return true; +} + +bool verify_dir(const test::path& dir) +{ + bool is_dir = false; + if (!winapi::ex::wide::fileExists(dir.c_str(), &is_dir) || !is_dir) { + std::wcerr << L"Directory does not exist: " << dir << std::endl; + return false; + } else + return true; +} + +std::wstring get_env_var(const wchar_t* var) +{ + using namespace std; + size_t size = GetEnvironmentVariableW(var, nullptr, 0); + if (size) { + std::wstring value(size, L'\0'); + size = GetEnvironmentVariableW(var, &value[0], static_cast<DWORD>(value.size())); + if (size && size < value.size()) { + value.resize(size); + return value; + } + } + wcerr << L"GetEnvironmentVariableW(" << var << L") failed result=" << size + << L" error=" << GetLastError() << endl; + return wstring(); +} + +bool set_env_var(const wchar_t* var, const std::wstring& value) +{ + if (SetEnvironmentVariableW(var, value.c_str())) + return true; + std::wcerr << L"SetEnvironmentVariableW(" << var << L"," << value + << L") failed error=" << GetLastError() << std::endl; + return false; +} + +int wmain(int argc, wchar_t* argv[]) +{ + using namespace std; + using namespace test; + + const wchar_t* label = nullptr; + wstring scenario; + path temp; + usvfs_test_options options; + + for (int ai = 1; ai < argc; ++ai) { + if (wcscmp(argv[ai], L"-ops32") == 0) + options.set_ops32(); + else if (wcscmp(argv[ai], L"-ops64") == 0) + options.set_ops64(); + else if (wcscmp(argv[ai], L"-opsexe") == 0) { + if (!verify_args_exist(L"-opsexe", 1, ai, argc)) + return 1; + options.opsexe = argv[++ai]; + if (!verify_file(options.opsexe)) + return 1; + } else if (wcscmp(argv[ai], L"-opsarg") == 0) { + if (!verify_args_exist(L"-opsarg", 1, ai, argc)) + return 1; + options.add_ops_options(argv[++ai]); + } else if (wcscmp(argv[ai], L"-fixture") == 0) { + if (!verify_args_exist(L"-fixture", 1, ai, argc)) + return 1; + options.fixture = argv[++ai]; + if (!verify_dir(options.fixture)) + return 1; + } else if (wcscmp(argv[ai], L"-mapping") == 0) { + if (!verify_args_exist(L"-mapping", 1, ai, argc)) + return 1; + options.mapping = argv[++ai]; + if (!verify_file(options.mapping)) + return 1; + } else if (wcscmp(argv[ai], L"-temp") == 0) { + if (!verify_args_exist(L"-temp", 1, ai, argc)) + return 1; + options.mount = argv[++ai]; + } else if (wcscmp(argv[ai], L"-mount") == 0) { + if (!verify_args_exist(L"-mount", 1, ai, argc)) + return 1; + options.mount = argv[++ai]; + } else if (wcscmp(argv[ai], L"-source") == 0) { + if (!verify_args_exist(L"-source", 1, ai, argc)) + return 1; + options.source = argv[++ai]; + } else if (wcscmp(argv[ai], L"-out") == 0) { + if (!verify_args_exist(L"-out", 1, ai, argc)) + return 1; + options.output = argv[++ai]; + } else if (wcscmp(argv[ai], L"-usvfslog") == 0) { + if (!verify_args_exist(L"-usvfslog", 1, ai, argc)) + return 1; + options.usvfs_log = argv[++ai]; + } else if (wcscmp(argv[ai], L"-forcetemprecursivedelete") == 0) + options.force_temp_cleanup = true; + else if (argv[ai][0] == '-') { + wcerr << L"Unknown option " << argv[ai] << endl; + return 1; + } else if (!scenario.empty()) { + wcerr << L"Multiple scenarios can not be specified: " << scenario.c_str() << L", " + << argv[ai] << endl; + return 1; + } else { + label = wcschr(argv[ai], ':'); + if (label) { + scenario = std::wstring(static_cast<const wchar_t*>(argv[ai]), label); + ++label; // skip the ':' + } else + scenario = argv[ai]; + + if (scenario.empty()) { + wcerr << L"Scenario name can not be empty!" << endl; + return 1; + } + } + } + + path test_name{"usvfs_test"}; + + if (scenario.empty()) { + print_usage(path(argv[0]).stem(), test_name); + return 1; + } + + options.fill_defaults(test_name, scenario, label); + + unique_ptr<usvfs_test_base> test{find_scenario( + usvfs::shared::string_cast<std::string>(scenario, usvfs::shared::CodePage::UTF8), + options)}; + if (!test) { + wcerr << L"ERROR: Unknown scenario specified: " << scenario.c_str() << endl; + return 2; + } + + auto dllPath = path_of_usvfs_lib(platform_dependant_executable("usvfs", "dll")); + ScopedLoadLibrary loadDll(dllPath.c_str()); + if (!loadDll) { + wcerr << L"ERROR: failed to load usvfs dll: " << dllPath.c_str() << L", " + << GetLastError() << endl; + return 3; + } + + // In order for bin\usvfs_proxy_x??.exe to find the lib\usvfs_x??.dll file we add the + // lib folder to our path: + std::wstring env_path = get_env_var(L"Path"); + if (env_path.empty() || + !set_env_var(L"Path", dllPath.parent_path().wstring() + L";" + env_path)) { + wcerr << L"ERROR: failed to add usvfs dll directory to path" << endl; + return 3; + } + + return test->run(path(argv[0]).stem()); +} diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.cpp b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.cpp new file mode 100644 index 0000000..bfa391e --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.cpp @@ -0,0 +1,1002 @@ + +#include "usvfs_test_base.h" +#include <boost/filesystem.hpp> +#include <boost/type_traits.hpp> +#include <cctype> +#include <cerrno> +#include <chrono> +#include <future> +#include <iostream> +#include <stringcast.h> +#include <thread> +#include <unordered_set> +#include <usvfs.h> +#include <vector> +#include <winapi.h> + +using test::throw_testWinFuncFailed; + +// usvfs_test_options class: + +void usvfs_test_options::fill_defaults(const path& test_name, + const std::wstring& scenario, + const wchar_t* label) +{ + using namespace test; + + std::wstring lbl; + if (label) + lbl = label; + +#ifdef _WIN64 + set_ops64(); + if (!label) + lbl = L"x64"; +#else + set_ops32(); + if (!label) + lbl = L"x86"; +#endif + + std::wstring scenario_label = scenario; + if (!lbl.empty()) { + scenario_label += L"_"; + scenario_label += lbl; + } + + if (fixture.empty()) + fixture = path_of_test_fixtures(test_name / scenario); + + if (mapping.empty()) + mapping = fixture / DEFAULT_MAPPING; + + if (temp.empty()) { + temp = path_of_test_temp(test_name / scenario_label); + } + + if (mount.empty()) { + mount = temp / MOUNT_DIR; + temp_cleanup = true; + } + + if (source.empty()) { + source = temp / SOURCE_DIR; + temp_cleanup = true; + } + + if (output.empty()) { + output = temp / scenario_label; + output += ".log"; + } + + if (usvfs_log.empty()) { + usvfs_log = temp / scenario_label; + usvfs_log += "_usvfs.log"; + } +} + +void usvfs_test_options::set_ops32() +{ + if (opsexe.empty()) + opsexe = test::path_of_test_bin(L"test_file_operations_x86.exe"); +} + +void usvfs_test_options::set_ops64() +{ + if (opsexe.empty()) + opsexe = test::path_of_test_bin(L"test_file_operations_x64.exe"); +} + +void usvfs_test_options::add_ops_options(const std::wstring& options) +{ + if (!ops_options.empty()) + ops_options += L" "; + ops_options += options; +} + +// usvfs_connector helper class: + +class usvfs_connector +{ +public: + using path = test::path; + + usvfs_connector(const usvfs_test_options& options) + : m_usvfs_log(test::ScopedFILE::open(options.usvfs_log, L"wt")), + m_exit_future(m_exit_signal.get_future()) + { + winapi::ex::wide::createPath(options.usvfs_log.parent_path().c_str()); + + std::wcout << "Connecting VFS..." << std::endl; + + std::unique_ptr<usvfsParameters, decltype(&usvfsFreeParameters)> parameters{ + usvfsCreateParameters(), &usvfsFreeParameters}; + + usvfsSetInstanceName(parameters.get(), "usvfs_test"); + usvfsSetDebugMode(parameters.get(), false); + usvfsSetLogLevel(parameters.get(), LogLevel::Debug); + usvfsSetCrashDumpType(parameters.get(), CrashDumpsType::None); + usvfsSetCrashDumpPath(parameters.get(), ""); + + usvfsInitLogging(false); + usvfsCreateVFS(parameters.get()); + + m_log_thread = std::thread(&usvfs_connector::usvfs_logger, this); + } + + ~usvfs_connector() + { + usvfsDisconnectVFS(); + m_exit_signal.set_value(); + m_log_thread.join(); + } + + enum class map_type + { + none, // the mapping_reader uses this value but will never pass it to the + // usvfs_connector (which ignored such entries) + dir, + dircreate, + file + }; + + struct mapping + { + mapping(map_type itype, std::wstring isource, std::wstring idestination) + : type(itype), source(isource), destination(idestination) + {} + + map_type type; + path source; + path destination; + }; + + using mappings_list = std::vector<mapping>; + + void updateMapping(const mappings_list& mappings, const usvfs_test_options& options, + FILE* log) + { + using namespace std; + using namespace usvfs::shared; + + fprintf(log, "Updating VFS mappings:\n"); + + usvfsClearVirtualMappings(); + + for (const auto& map : mappings) { + const string& source = + usvfs_test_base::SOURCE_LABEL + + test::path_as_relative(options.source, map.source).string(); + const string& destination = + usvfs_test_base::MOUNT_LABEL + + test::path_as_relative(options.mount, map.destination).string(); + switch (map.type) { + case map_type::dir: + fprintf(log, " mapdir: %s => %s\n", source.c_str(), destination.c_str()); + usvfsVirtualLinkDirectoryStatic(map.source.c_str(), map.destination.c_str(), + LINKFLAG_RECURSIVE); + break; + + case map_type::dircreate: + fprintf(log, " mapdircreate: %s => %s\n", source.c_str(), destination.c_str()); + usvfsVirtualLinkDirectoryStatic(map.source.c_str(), map.destination.c_str(), + LINKFLAG_CREATETARGET | LINKFLAG_RECURSIVE); + break; + + case map_type::file: + fprintf(log, " mapfile: %s => %s\n", source.c_str(), destination.c_str()); + usvfsVirtualLinkFile(map.source.c_str(), map.destination.c_str(), + LINKFLAG_RECURSIVE); + break; + } + } + + fprintf(log, "\n"); + } + + static DWORD spawn(wchar_t* commandline) + { + using namespace usvfs::shared; + + STARTUPINFO si{0}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi{0}; + + if (!usvfsCreateProcessHooked(NULL, commandline, NULL, NULL, FALSE, 0, NULL, NULL, + &si, &pi)) + throw_testWinFuncFailed( + "CreateProcessHooked", + string_cast<std::string>(commandline, CodePage::UTF8).c_str()); + + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD exit = 99; + if (!GetExitCodeProcess(pi.hProcess, &exit)) { + test::WinFuncFailedGenerator failed; + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + throw failed("GetExitCodeProcess"); + } + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + return exit; + } + + void usvfs_logger() + { + fprintf(m_usvfs_log, "usvfs_test usvfs logger started:\n"); + fflush(m_usvfs_log); + + constexpr size_t size = 1024; + char buf[size + 1]{0}; + int noLogCycles = 0; + std::chrono::milliseconds wait_for; + do { + if (usvfsGetLogMessages(buf, size, false)) { + fwrite(buf, 1, strlen(buf), m_usvfs_log); + fwrite("\n", 1, 1, m_usvfs_log); + fflush(m_usvfs_log); + noLogCycles = 0; + } else + ++noLogCycles; + if (noLogCycles) + wait_for = std::chrono::milliseconds(std::min(40, noLogCycles) * 5); + else + wait_for = std::chrono::milliseconds(0); + } while (m_exit_future.wait_for(wait_for) == std::future_status::timeout); + + while (usvfsGetLogMessages(buf, size, false)) { + fwrite(buf, 1, strlen(buf), m_usvfs_log); + fwrite("\n", 1, 1, m_usvfs_log); + } + + fprintf(m_usvfs_log, "usvfs log closed.\n"); + m_usvfs_log.close(); + } + +private: + test::ScopedFILE m_usvfs_log; + std::thread m_log_thread; + std::promise<void> m_exit_signal; + std::shared_future<void> m_exit_future; +}; + +// mappings_reader + +class mappings_reader +{ +public: + using path = test::path; + using string = std::string; + using wstring = std::wstring; + using map_type = usvfs_connector::map_type; + using mapping = usvfs_connector::mapping; + using mappings_list = usvfs_connector::mappings_list; + + mappings_reader(const path& mount_base, const path& source_base) + : m_mount_base(mount_base), m_source_base(source_base) + {} + + mappings_list read(const path& mapfile) + { + const auto map = test::ScopedFILE::open(mapfile, L"rt"); + mappings_list mappings; + + char line[1024]; + while (!feof(map)) { + // read one line: + if (!fgets(line, _countof(line), map)) + if (feof(map)) + break; + else + throw_testWinFuncFailed("fgets", "reading mappings"); + + if (empty_line(line)) + continue; + + if (start_nesting(line, "mapdir")) + m_nesting = map_type::dir; + else if (start_nesting(line, "mapdircreate")) + m_nesting = map_type::dircreate; + else if (start_nesting(line, "mapfile")) + m_nesting = map_type::file; + else if (!isspace(*line)) // mapping sources should be indented and we already + // check all the mapping directives + throw test::FuncFailed("mappings_reader::read", "invalid mappings file line", + line); + else { + const auto& source_rel = trimmed_wide_string(line); + mappings.push_back(mapping(m_nesting, m_source_base / source_rel, m_mount)); + } + } + + return mappings; + } + + bool start_nesting(const char* line, const char* directive) + { + // check if line starts with directive and if so skip it: + auto dlen = strlen(directive); + auto after = line + dlen; + if (strncmp(directive, line, dlen) == 0 && (!*after || isspace(*after))) { + m_mount = m_mount_base; + const auto& mount_rel = trimmed_wide_string(after); + if (!mount_rel.empty()) + m_mount /= mount_rel; + return true; + } else + return false; + } + + static wstring trimmed_wide_string(const char* in) + { + while (std::isspace(*in)) + ++in; + auto end = in; + end += strlen(end); + while (end > in && std::isspace(*(end - 1))) + --end; + return usvfs::shared::string_cast<wstring>(string(in, end), + usvfs::shared::CodePage::UTF8); + } + + static bool empty_line(const char* line) + { + for (; *line; ++line) { + if (*line == '#') // comment, ignore rest of line + return true; + else if (!std::isspace(*line)) + return false; + } + return true; + } + +private: + path m_mount_base; + path m_source_base; + path m_mount; + map_type m_nesting = map_type::none; +}; + +// usvfs_test_base class: + +void usvfs_test_base::cleanup_temp() +{ + using namespace test; + using namespace winapi::ex::wide; + + bool isDir = false; + if (!m_o.temp_cleanup || !fileExists(m_o.temp.c_str(), &isDir)) + return; + + if (!isDir) { + if (m_o.force_temp_cleanup) + delete_file(m_o.temp); + else + throw FuncFailed("cleanup_temp", "temp exists but is a file", + m_o.temp.string().c_str()); + } else { + std::vector<wstring> cleanfiles; + std::vector<wstring> cleandirs; + std::vector<wstring> otherdirs; + bool output_file = false; + for (auto f : quickFindFiles(m_o.temp.c_str(), L"*")) + if (f.fileName == L"." || f.fileName == L"..") + continue; + else if ((f.attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { + if (f.fileName == m_o.output.filename()) + output_file = true; + cleanfiles.push_back(f.fileName); + } else if (f.fileName == SOURCE_DIR || f.fileName == MOUNT_DIR) + cleandirs.push_back(f.fileName); + else + otherdirs.push_back(f.fileName); + if (!cleanfiles.empty() || !cleandirs.empty() || !otherdirs.empty()) { + if (!m_o.force_temp_cleanup && !otherdirs.empty()) + throw FuncFailed("cleanup_temp", + "Refusing to clean temp dir with non-mount/source directories " + "(clean manually and rerun)", + m_o.temp.string().c_str()); + if (!m_o.force_temp_cleanup && cleandirs.empty() && !output_file) + throw FuncFailed("cleanup_temp", + "Refusing to clean temp dir with no directories and no output " + "log (clean manually and rerun)", + m_o.temp.string().c_str()); + std::wcout << "Cleaning previous temp dir: " << m_o.temp.c_str() << std::endl; + for (auto f : cleanfiles) + delete_file(m_o.temp / f); + for (auto d : cleandirs) + recursive_delete_files(m_o.temp / d); + for (auto d : otherdirs) + recursive_delete_files(m_o.temp / d); + } + } +} + +void usvfs_test_base::copy_fixture() +{ + using namespace test; + using namespace winapi::ex::wide; + + path fmount = m_o.fixture / MOUNT_DIR; + path fsource = m_o.fixture / SOURCE_DIR; + + bool isDir = false; + if (!fileExists(fmount.c_str(), &isDir) || !isDir) + throw FuncFailed("copy_fixture", "fixtures dir does not exist", + fmount.string().c_str()); + if (!fileExists(fsource.c_str(), &isDir) || !isDir) + throw FuncFailed("copy_fixture", "fixtures dir does not exist", + fsource.string().c_str()); + if (fileExists(m_o.mount.c_str(), &isDir)) + throw FuncFailed("copy_fixture", "source dir already exists", + m_o.mount.string().c_str()); + if (fileExists(m_o.source.c_str(), &isDir)) + throw FuncFailed("copy_fixture", "source dir already exists", + m_o.source.string().c_str()); + + std::wcout << "Copying fixture: " << m_o.fixture << std::endl; + recursive_copy_files(fmount, m_o.mount, false); + recursive_copy_files(fsource, m_o.source, false); +} + +bool usvfs_test_base::postmortem_check() +{ + path gold_output = m_o.fixture / m_o.output.filename(); + + { + const auto& log = output(); + + path mount_gold = MOUNT_DIR; + mount_gold += POSTMORTEM_SUFFIX; + path source_gold = SOURCE_DIR; + source_gold += POSTMORTEM_SUFFIX; + + bool is_dir = false; + if (!winapi::ex::wide::fileExists(m_o.mount.c_str(), &is_dir) || !is_dir) { + fprintf(log, " ERROR: mount directory does not exist?!\n"); + return false; + } + if (!winapi::ex::wide::fileExists(m_o.source.c_str(), &is_dir) || !is_dir) { + fprintf(log, " ERROR: source directory does not exist?!\n"); + return false; + } + if (!winapi::ex::wide::fileExists((m_o.fixture / mount_gold).c_str(), &is_dir) || + !is_dir) { + fprintf(log, " ERROR: fixtures golden mount does not exist: %s\n", + mount_gold.string().c_str()); + return false; + } + if (!winapi::ex::wide::fileExists((m_o.fixture / source_gold).c_str(), &is_dir) || + !is_dir) { + fprintf(log, " ERROR: fixtures golden source does not exist: %s\n", + mount_gold.string().c_str()); + return false; + } + if (!winapi::ex::wide::fileExists(gold_output.c_str(), &is_dir) || is_dir) { + fprintf(log, " ERROR: golden scenario output does not exist: %s\n", + gold_output.filename().string().c_str()); + return false; + } + + fprintf(log, "postmortem check of %s against golden %s...\n", + m_o.mount.filename().string().c_str(), mount_gold.string().c_str()); + bool mount_check = + recursive_compare_dirs(path(), m_o.fixture / mount_gold, m_o.mount, log); + + fprintf(log, "postmortem check of %s against golden %s...\n", + m_o.source.filename().string().c_str(), source_gold.string().c_str()); + bool source_check = + recursive_compare_dirs(path(), m_o.fixture / source_gold, m_o.source, log); + + if (mount_check && source_check) + fprintf(log, "postmortem check successful.\n"); + else { + fprintf(log, "ERROR: postmortem check failed!\n"); + return false; + } + } // close output before comparing it + + // don't print anything more to the output (except maybe errors), + // so that the final output can be copied as is to the fixtures (when updating the + // golden version) + + // block remove 2024/06/07 - one would need to regenerate a gold output for this but + // I am not sure this is still relevant + // + // if (!test::compare_files(gold_output, m_o.output, false)) { + // fprintf(output(), "ERROR: output does not match gold output: %s\n", + // m_o.output.filename().string().c_str()); return false; + //} + + return true; +} + +bool usvfs_test_base::recursive_compare_dirs(path rel_path, path gold_base, + path result_base, FILE* log) +{ + path result_full = result_base / rel_path; + path gold_full = gold_base / rel_path; + + std::unordered_set<std::wstring> gold_dirs; + std::unordered_set<std::wstring> gold_files; + for (const auto& f : winapi::ex::wide::quickFindFiles(gold_full.c_str(), L"*")) { + if (f.fileName == L"." || f.fileName == L"..") + continue; + if (f.attributes & FILE_ATTRIBUTE_DIRECTORY) + gold_dirs.insert(f.fileName); + else + gold_files.insert(f.fileName); + } + + bool all_good = true; + + std::vector<std::wstring> recurse; + for (const auto& f : winapi::ex::wide::quickFindFiles(result_full.c_str(), L"*")) { + if (f.fileName == L"." || f.fileName == L"..") + continue; + if (f.attributes & FILE_ATTRIBUTE_DIRECTORY) { + const auto& find = gold_dirs.find(f.fileName); + if (find != gold_dirs.end()) { + gold_dirs.erase(find); + recurse.push_back(f.fileName); + } else { + fprintf(log, " unexpected directory found: %s%s\n", MOUNT_LABEL, + (rel_path / f.fileName).string().c_str()); + all_good = false; + } + } else { + const auto& find = gold_files.find(f.fileName); + if (find != gold_files.end()) { + gold_files.erase(find); + if (!test::compare_files(gold_full / f.fileName, result_full / f.fileName, + false)) { + fprintf(log, " file contents differs: %s%s\n", MOUNT_LABEL, + (rel_path / f.fileName).string().c_str()); + all_good = false; + } + } else { + fprintf(log, " unexpected file found: %s%s\n", MOUNT_LABEL, + (rel_path / f.fileName).string().c_str()); + all_good = false; + } + } + } + + for (auto d : gold_dirs) { + fprintf(log, " expected directory not found: %s%s\n", MOUNT_LABEL, + (rel_path / d).string().c_str()); + all_good = false; + } + + for (auto f : gold_files) { + fprintf(log, " expected file not found: %s%s\n", MOUNT_LABEL, + (rel_path / f).string().c_str()); + all_good = false; + } + + for (auto r : recurse) + all_good &= recursive_compare_dirs(rel_path / r, gold_base, result_base, log); + + return all_good; +} + +test::ScopedFILE usvfs_test_base::output() +{ + auto log = test::ScopedFILE::open(m_o.output, m_clean_output ? L"wt" : L"at"); + m_clean_output = false; + return log; +} + +void usvfs_test_base::clean_output() +{ + using namespace std; + + errno_t err; + auto in = test::ScopedFILE::open(m_o.output, L"rt", err); + if (err == ENOENT) { + wcerr << L"warning: no " << m_o.output << L" to clean." << endl; + return; + } else if (err || !in) + throw_testWinFuncFailed("_wfopen_s", m_o.output.string().c_str(), err); + + path clean = m_o.output.parent_path() / m_o.output.stem(); + clean += OUTPUT_CLEAN_SUFFIX; + clean += m_o.output.extension(); + + auto out = test::ScopedFILE::open(clean, L"wt"); + wcout << L"Cleaning " << m_o.output << " to " << clean << endl; + + char line[1024]; + while (!feof(in)) { + // read one line: + if (!fgets(line, _countof(line), in)) + if (feof(in)) + break; + else + throw_testWinFuncFailed("fgets", "reading output"); + if (*line == '#') + continue; + + // in order for the clean output to compare cleanly between run with different + // options we clean out things like the platform and the ops log name (which + // contians the scenario label): + + char* platform = line; + while (platform) { + char* platform_x86 = strstr(platform, "x86"); + char* platform_x64 = strstr(platform, "x64"); + if (platform_x86 && platform_x64) + platform = std::min(platform_x86, platform_x64); + else if (platform_x86) + platform = platform_x86; + else if (platform_x64) + platform = platform_x64; + else + platform = nullptr; + if (platform) { + platform[1] = platform[2] = '?'; + platform += 3; + } + } + + char* cout_end = strstr(line, "-cout+ "); + char* cout_log_end = nullptr; + if (cout_end) { + cout_end += strlen("-cout+ "); + cout_log_end = strchr(cout_end, ' '); + } + if (cout_log_end && cout_log_end > cout_end) { + cout_end[0] = '?'; + if (cout_log_end > cout_end + 1) + memmove(cout_end + 1, cout_log_end, strlen(cout_log_end) + 1); + } + + fputs(line, out); + } +} + +int usvfs_test_base::run(const std::wstring& exe_name) +{ + using namespace usvfs::shared; + using namespace std; + + int res = run_impl(exe_name); + try { + clean_output(); + } catch (const exception& e) { + wcerr << "CERROR: " << string_cast<wstring>(e.what(), CodePage::UTF8).c_str() + << endl; + } catch (...) { + wcerr << "CERROR: unknown exception" << endl; + } + if (!res) + wcout << "scenario " << scenario_name() << " PASSED." << endl; + else + wcerr << "scenario " << scenario_name() << " FAILED!" << endl; + return res; +} + +int usvfs_test_base::run_impl(const std::wstring& exe_name) +{ + using namespace usvfs::shared; + using namespace std; + + try { + winapi::ex::wide::createPath(m_o.output.parent_path().c_str()); + + // we read mappings first only because it is "non-destructive" but might raise an + // error if mappings invalid + auto mappings = mappings_reader(m_o.mount, m_o.source).read(m_o.mapping); + + cleanup_temp(); + log_settings(exe_name); + copy_fixture(); + + usvfs_connector usvfs(m_o); + { + const auto& log = output(); + usvfs.updateMapping(mappings, m_o, log); + + fprintf(log, "running scenario %s:\n\n", scenario_name()); + } + auto res = scenario_run(); + { + const auto& log = output(); + if (res) + fprintf(log, "\nscenario ended successfully!\n\n"); + else + fprintf(log, "\nscenario failed miserably.\n"); + } + + if (!res) + return 7; + + if (!postmortem_check()) + return 8; + + return 0; + } +#if 1 // just a convient way to not catch exception when debugging + catch (const exception& e) { + try { + wcerr << "ERROR: " << string_cast<wstring>(e.what(), CodePage::UTF8).c_str() + << endl; + fprintf(output(), "ERROR: %s\n", e.what()); + } catch (const exception& e) { + wcerr << "ERROR^2: " << string_cast<wstring>(e.what(), CodePage::UTF8).c_str() + << endl; + } catch (...) { + wcerr << "ERROR^2: unknown exception" << endl; + } + } catch (...) { + try { + wcerr << "ERROR: unknown exception" << endl; + fprintf(output(), "ERROR: unknown exception\n"); + } catch (const exception& e) { + wcerr << "ERROR^2: " << string_cast<wstring>(e.what(), CodePage::UTF8).c_str() + << endl; + } catch (...) { + wcerr << "ERROR^2: unknown exception" << endl; + } + } +#else + catch (bool) { + } +#endif + return 9; // exception +} + +void usvfs_test_base::log_settings(const std::wstring& exe_name) +{ + using namespace usvfs::shared; + fprintf(output(), "%s %s started with %s%s%s\n\n", + string_cast<std::string>(exe_name).c_str(), scenario_name(), + m_o.opsexe.filename().string().c_str(), m_o.ops_options.empty() ? "" : " ", + string_cast<std::string>(m_o.ops_options).c_str()); +} + +void usvfs_test_base::ops_list(const path& rel_path, bool recursive, bool with_contents, + bool should_succeed, const wstring& additional_args) +{ + wstring cmd = recursive ? L"-r -list" : L"-list"; + if (with_contents) + cmd += L"contents"; + run_ops(should_succeed, cmd, rel_path, additional_args); +} + +void usvfs_test_base::ops_read(const path& rel_path, bool should_succeed, + const wstring& additional_args) +{ + run_ops(should_succeed, L"-read", rel_path, additional_args); +} + +void usvfs_test_base::ops_rewrite(const path& rel_path, const char* contents, + bool should_succeed, const wstring& additional_args) +{ + using namespace usvfs::shared; + run_ops(should_succeed, L"-rewrite", rel_path, additional_args, + L"\"" + string_cast<wstring>(contents, CodePage::UTF8) + L"\""); +} + +void usvfs_test_base::ops_overwrite(const path& rel_path, const char* contents, + bool recursive, bool should_succeed, + const wstring& additional_args) +{ + using namespace usvfs::shared; + run_ops(should_succeed, recursive ? L"-r -overwrite" : L"-overwrite", rel_path, + additional_args, + L"\"" + string_cast<wstring>(contents, CodePage::UTF8) + L"\""); +} + +void usvfs_test_base::ops_touch(const path& rel_path, bool full_write_access, + bool should_succeed, const wstring& additional_args) +{ + run_ops(should_succeed, full_write_access ? L"-touchw" : L"-touch", rel_path, + additional_args); +} + +void usvfs_test_base::ops_deleteoverwrite(const path& rel_path, const char* contents, + bool recursive, bool should_succeed, + const wstring& additional_args) +{ + using namespace usvfs::shared; + run_ops(should_succeed, recursive ? L"-r -deleteoverwrite" : L"-deleteoverwrite", + rel_path, additional_args, + L"\"" + string_cast<wstring>(contents, CodePage::UTF8) + L"\""); +} + +void usvfs_test_base::ops_delete(const path& rel_path, bool should_succeed, + const wstring& additional_args) +{ + run_ops(should_succeed, L"-delete", rel_path, additional_args); +} + +void usvfs_test_base::ops_copy(const path& src_rel_path, const path& dest_rel_path, + bool replace, bool should_succeed, + const wstring& additional_args) +{ + run_ops(should_succeed, replace ? L"-copyover" : L"-copy", src_rel_path, + additional_args, wstring(), dest_rel_path); +} + +void usvfs_test_base::ops_rename(const path& src_rel_path, const path& dest_rel_path, + bool replace, bool allow_copy, bool should_succeed, + const wstring& additional_args) +{ + wstring command = allow_copy ? L"-move" : L"-rename"; + if (replace) + command += L"over"; + run_ops(should_succeed, command, src_rel_path, additional_args, wstring(), + dest_rel_path); +} + +void usvfs_test_base::ops_deleterename(const path& src_rel_path, + const path& dest_rel_path, bool allow_copy, + bool should_succeed, + const wstring& additional_args) +{ + wstring command = allow_copy ? L"-deletemove" : L"-deleterename"; + run_ops(should_succeed, command, src_rel_path, additional_args, wstring(), + dest_rel_path); +} + +void usvfs_test_base::run_ops(bool should_succeed, const wstring& preargs, + const path& rel_path, const wstring& additional_args, + const wstring& postargs, const path& rel_path2) +{ + using namespace usvfs::shared; + using string = std::string; + using wstring = wstring; + + string commandlog = test::path(m_o.opsexe).filename().string(); + wstring commandline = m_o.opsexe; + if (commandline.find(' ') != wstring::npos && + commandline.find('"') == wstring::npos) { + commandline = L"\"" + commandline + L"\""; + commandlog = "\"" + commandlog + "\""; + } + + if (!m_o.mount.empty()) { + commandline += L" -basedir "; + commandline += m_o.mount; + commandlog += " -basedir "; + commandlog += m_o.mount.filename().string(); + } + + if (!m_o.ops_options.empty()) { + commandline += L" "; + commandline += m_o.ops_options; + commandlog += " "; + commandlog += string_cast<string>(m_o.ops_options, CodePage::UTF8); + } + + commandline += L" -cout+ "; + commandline += m_o.output; + commandlog += " -cout+ "; + commandlog += m_o.output.filename().string(); + + if (!additional_args.empty()) { + commandline += L" "; + commandline += additional_args; + commandlog += " "; + commandlog += string_cast<string>(additional_args, CodePage::UTF8); + } + + if (!preargs.empty()) { + commandline += L" "; + commandline += preargs; + commandlog += " "; + commandlog += string_cast<string>(preargs, CodePage::UTF8); + } + + if (!rel_path.empty()) { + commandline += L" "; + commandline += m_o.mount / rel_path; + commandlog += " "; + commandlog += MOUNT_LABEL + rel_path.string(); + } + + if (!rel_path2.empty()) { + commandline += L" "; + commandline += m_o.mount / rel_path2; + commandlog += " "; + commandlog += MOUNT_LABEL + rel_path2.string(); + } + + if (!postargs.empty()) { + commandline += L" "; + commandline += postargs; + commandlog += " "; + commandlog += string_cast<string>(postargs, CodePage::UTF8); + } + + fprintf(output(), "Spawning: %s\n", commandlog.c_str()); + auto res = usvfs_connector::spawn(&commandline[0]); + fprintf(output(), "\n"); + + bool success = res == 0; + if (success != should_succeed) + throw test::FuncFailed("run_ops", success ? "succeeded" : "failed", + commandlog.c_str(), res); +} + +std::string usvfs_test_base::mount_contents(const path& rel_path) +{ + verify_mount_existance(rel_path); + const auto& contents = test::read_small_file(m_o.mount / rel_path); + return std::string(contents.data(), contents.size()); +} + +std::string usvfs_test_base::source_contents(const path& rel_path) +{ + verify_source_existance(rel_path); + const auto& contents = test::read_small_file(m_o.source / rel_path); + return std::string(contents.data(), contents.size()); +} + +void usvfs_test_base::verify_mount_contents(const path& rel_path, const char* contents) +{ + verify_mount_existance(rel_path); + if (verify_contents(m_o.mount / rel_path, contents)) + throw test::FuncFailed("verify_mount_contents", + (MOUNT_LABEL + rel_path.string()).c_str(), contents); +} + +void usvfs_test_base::verify_source_contents(const path& rel_path, const char* contents) +{ + verify_source_existance(rel_path); + if (verify_contents(m_o.source / rel_path, contents)) + throw test::FuncFailed("verify_source_contents", + (SOURCE_LABEL + rel_path.string()).c_str(), contents); +} + +bool usvfs_test_base::verify_contents(const path& file, const char* contents) +{ + // we allow difference in trailing whitespace (i.e. extra new line): + + size_t sz = strlen(contents); + while (sz && isspace(contents[sz - 1])) + --sz; + + const auto& real_contents = test::read_small_file(file); + size_t real_sz = real_contents.size(); + while (real_sz && isspace(real_contents[real_sz - 1])) + --real_sz; + + return sz == real_sz && memcmp(contents, real_contents.data(), sz); +} + +void usvfs_test_base::verify_mount_existance(const path& rel_path, bool exists, + bool is_dir) +{ + bool real_is_dir = false; + bool real_exists = + winapi::ex::wide::fileExists((m_o.mount / rel_path).c_str(), &real_is_dir); + if (exists != real_exists) + throw test::FuncFailed("verify_mount_existance", + real_exists ? "path exists" : "path does not exist", + (MOUNT_LABEL + rel_path.string()).c_str()); + else if (real_exists && is_dir != real_is_dir) + throw test::FuncFailed("verify_mount_existance", + real_is_dir ? "path is a directory" : "path is a file", + (MOUNT_LABEL + rel_path.string()).c_str()); +} + +void usvfs_test_base::verify_source_existance(const path& rel_path, bool exists, + bool is_dir) +{ + bool real_is_dir = false; + bool real_exists = + winapi::ex::wide::fileExists((m_o.source / rel_path).c_str(), &real_is_dir); + if (exists != real_exists) + throw test::FuncFailed("verify_source_existance", + real_exists ? "path exists" : "path does not exist", + (SOURCE_LABEL + rel_path.string()).c_str()); + else if (real_exists && is_dir != real_is_dir) + throw test::FuncFailed("verify_source_existance", + real_is_dir ? "path is a directory" : "path is a file", + (SOURCE_LABEL + rel_path.string()).c_str()); +} diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.h b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.h new file mode 100644 index 0000000..4986e3b --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.h @@ -0,0 +1,120 @@ +#pragma once + +#include <string> + +#include <test_helpers.h> + +class usvfs_test_options +{ +public: + static constexpr auto DEFAULT_MAPPING = L"vfs_mappings.txt"; + static constexpr auto MOUNT_DIR = L"mount"; + static constexpr auto SOURCE_DIR = L"source"; + + using path = test::path; + + // fills any values not set (or set to an empty value) to their default value + void fill_defaults(const path& test_name, const std::wstring& scenario, + const wchar_t* label); + + void set_ops32(); // sets opsexe iff opsexe is empty + void set_ops64(); // sets opsexe iff opsexe is empty + void add_ops_options(const std::wstring& options); + + path opsexe; + path fixture; + path mapping; + path temp; + path mount; + path source; + path output; + path usvfs_log; + std::wstring ops_options; + bool temp_cleanup = false; + bool force_temp_cleanup = false; +}; + +class usvfs_test_base +{ +public: + static constexpr auto MOUNT_DIR = usvfs_test_options::MOUNT_DIR; + static constexpr auto SOURCE_DIR = usvfs_test_options::SOURCE_DIR; + static constexpr auto MOUNT_LABEL = "mount:"; + static constexpr auto SOURCE_LABEL = "source:"; + static constexpr auto OUTPUT_CLEAN_SUFFIX = L"_clean"; + static constexpr auto POSTMORTEM_SUFFIX = L".postmortem"; + + using wstring = std::wstring; + using path = test::path; + + // options object should outlive this object. + usvfs_test_base(const usvfs_test_options& options) : m_o(options) {} + virtual ~usvfs_test_base() = default; + + int run(const std::wstring& exe_name); + + // function for override: + + virtual const char* scenario_name() = 0; + virtual bool scenario_run() = 0; + + // helpers for derived scenarios: + + virtual void ops_list(const path& rel_path, bool recursive, bool with_contents, + bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_read(const path& rel_path, bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_rewrite(const path& rel_path, const char* contents, + bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_overwrite(const path& rel_path, const char* contents, bool recursive, + bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_touch(const path& rel_path, bool full_write_access = false, + bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_deleteoverwrite(const path& rel_path, const char* contents, + bool recursive, bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_delete(const path& rel_path, bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_copy(const path& src_rel_path, const path& dest_rel_path, + bool replace, bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_rename(const path& src_rel_path, const path& dest_rel_path, + bool replace, bool allow_copy = false, + bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_deleterename(const path& src_rel_path, const path& dest_rel_path, + bool allow_copy = false, bool should_succeed = true, + const wstring& additional_args = wstring()); + + virtual std::string mount_contents(const path& rel_path); + virtual void verify_mount_contents(const path& rel_path, const char* contents); + virtual void verify_mount_existance(const path& rel_path, bool exists = true, + bool is_dir = false); + virtual std::string source_contents(const path& rel_path); + virtual void verify_source_contents(const path& rel_path, const char* contents); + virtual void verify_source_existance(const path& rel_path, bool exists = true, + bool is_dir = false); + +private: + int run_impl(const std::wstring& exe_name); + void log_settings(const std::wstring& exe_name); + void cleanup_temp(); + void copy_fixture(); + bool postmortem_check(); + bool recursive_compare_dirs(path rel_path, path gold_base, path result_base, + FILE* log); + void clean_output(); + + test::ScopedFILE output(); + void run_ops(bool should_succeed, const wstring& preargs, const path& rel_path, + const wstring& additional_args, const wstring& postargs = wstring(), + const path& rel_path2 = path()); + bool verify_contents(const path& file, const char* contents); + + const usvfs_test_options& m_o; + bool m_clean_output = true; +}; |
