Move file versioning functions out of DatabaseInterface

They don't need to be member functions, and needing a pointer to an
instance of that interface is awkward.

The function names have also changed to reflect that they're not
specific to the masterlists.
This commit is contained in:
Oliver Hamlet
2021-09-23 13:31:34 +01:00
parent 5ce0df0f03
commit e5a229e09f
10 changed files with 295 additions and 248 deletions
+1
View File
@@ -311,6 +311,7 @@ set(LIBLOOT_TESTS_SRC "${CMAKE_SOURCE_DIR}/src/tests/api/interface/main.cpp")
set(LIBLOOT_TESTS_HEADERS "${CMAKE_SOURCE_DIR}/src/tests/api/interface/api_game_operations_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/interface/create_game_handle_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/interface/database_interface_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/interface/file_versioning_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/interface/game_interface_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/interface/is_compatible_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/common_game_test_fixture.h")
+6
View File
@@ -33,6 +33,12 @@ Functions
.. doxygenfunction:: loot::CreateGameHandle
.. doxygenfunction:: loot::UpdateFile
.. doxygenfunction:: loot::GetFileRevision
.. doxygenfunction:: loot::IsLatestFile
Interfaces
==========
+68
View File
@@ -110,6 +110,74 @@ LOOT_API std::shared_ptr<GameInterface> CreateGameHandle(
const GameType game,
const std::filesystem::path& game_path,
const std::filesystem::path& game_local_path = "");
/**@}*/
/**********************************************************************/ /**
* @name File Versioning Functions
*************************************************************************/
/**@{*/
/**
* @brief Update the given masterlist or masterlist prelude file.
* @details Uses Git to update the given file using a given remote.
* If the file doesn't exist, this will create it. This
* function also initialises a Git repository in the given
* file's parent folder.
*
* If a Git repository is already present, it will be used to
* perform a diff-only update, but if for any reason a
* fast-forward merge update is not possible, the existing
* repository will be deleted and a new repository cloned from
* the given remote.
* @param file_path
* The relative or absolute path to the file that should be
* updated. The filename must match the filename of the file in the
* given remote repository, otherwise it will not be updated
* correctly. The file must be present in the repository's root
* directory.
* @param remote_url
* The URL of the remote from which to fetch updates. This can also be
* a relative or absolute path to a local repository.
* @param remote_branch
* The branch of the remote from which to apply updates.
* @returns `true` if the file was updated. `false` if no update was
* necessary, ie. it was already up-to-date.
*/
LOOT_API bool UpdateFile(const std::filesystem::path& file_path,
const std::string& remote_url,
const std::string& remote_branch);
/**
* @brief Get the given masterlist or masterlist prelude file's revision.
* @details Getting a file's revision is only possible if it is found
* in the root of a local Git repository.
* @param file_path
* The relative or absolute path to the file that should be queried.
* @param get_short_id
* If `true`, the shortest unique hexadecimal revision hash that is at
* least 7 characters long will be outputted. Otherwise, the full 40
* character hash will be outputted.
* @returns The revision data.
*/
LOOT_API FileRevision GetFileRevision(const std::filesystem::path& file_path,
const bool get_short_id);
/**
* Check if the given masterlist or masterlist prelude file is the latest
* available for a given branch.
* @param file_path
* The relative or absolute path to the file for which the latest
* revision should be obtained. It needs to be in the root of a local
* Git repository.
* @param branch
* The branch to check against.
* @return True if the file's current revision matches its latest revision
* for the given branch, and false otherwise.
*/
LOOT_API bool IsLatestFile(const std::filesystem::path& file_path,
const std::string& branch);
}
#endif
-71
View File
@@ -90,74 +90,6 @@ public:
virtual void WriteMinimalList(const std::filesystem::path& outputFile,
const bool overwrite) const = 0;
/**
* @}
* @name Masterlist Update
* @{
*/
/**
* @brief Update the given masterlist or masterlist prelude file.
* @details Uses Git to update the given file using a given remote.
* If the file doesn't exist, this will create it. This
* function also initialises a Git repository in the given
* file's parent folder.
*
* If a Git repository is already present, it will be used to
* perform a diff-only update, but if for any reason a
* fast-forward merge update is not possible, the existing
* repository will be deleted and a new repository cloned from
* the given remote.
* @param file_path
* The relative or absolute path to the file that should be
* updated. The filename must match the filename of the file in the
* given remote repository, otherwise it will not be updated
* correctly. The file must be present in the repository's root
* directory.
* @param remote_url
* The URL of the remote from which to fetch updates. This can also be
* a relative or absolute path to a local repository.
* @param remote_branch
* The branch of the remote from which to apply updates.
* @returns `true` if the file was updated. `false` if no update was
* necessary, ie. it was already up-to-date. If `true`, the
* file will need to be re-loaded and re-evaluated separately.
*/
virtual bool UpdateMasterlist(const std::filesystem::path& file_path,
const std::string& remote_url,
const std::string& remote_branch) = 0;
/**
* @brief Get the given masterlist or masterlist prelude file's revision.
* @details Getting a file's revision is only possible if it is found
* in the root of a local Git repository.
* @param file_path
* The relative or absolute path to the file that should be queried.
* @param get_short_id
* If `true`, the shortest unique hexadecimal revision hash that is at
* least 7 characters long will be outputted. Otherwise, the full 40
* character hash will be outputted.
* @returns The revision data.
*/
virtual FileRevision GetMasterlistRevision(
const std::filesystem::path& file_path,
const bool get_short_id) const = 0;
/**
* Check if the given masterlist or masterlist prelude file is the latest
* available for a given branch.
* @param file_path
* The relative or absolute path to the file for which the latest
* revision should be obtained. It needs to be in the root of a local
* Git repository.
* @param branch
* The branch to check against.
* @return True if the file's current revision matches its latest revision
* for the given branch, and false otherwise.
*/
virtual bool IsLatestMasterlist(const std::filesystem::path& file_path,
const std::string& branch) const = 0;
/**
* @}
* @name Non-plugin Data Access
@@ -230,9 +162,6 @@ public:
const std::string& fromGroupName,
const std::string& toGroupName) const = 0;
/**
* @brief Set the groups
/**
* @}
* @name Plugin Data Access
+23
View File
@@ -28,6 +28,7 @@
#include "api/game/game.h"
#include "api/helpers/logging.h"
#include "api/helpers/git.h"
namespace fs = std::filesystem;
@@ -97,4 +98,26 @@ LOOT_API std::shared_ptr<GameInterface> CreateGameHandle(
return std::make_shared<Game>(game, resolvedGamePath, resolvedGameLocalPath);
}
bool UpdateFile(const std::filesystem::path& masterlistPath,
const std::string& remoteURL,
const std::string& remoteBranch) {
if (!std::filesystem::is_directory(masterlistPath.parent_path()))
throw std::invalid_argument("The path \"" + masterlistPath.u8string() +
"\" does not have a valid parent directory.");
return git::UpdateFile(masterlistPath, remoteURL, remoteBranch);
}
FileRevision GetFileRevision(
const std::filesystem::path& masterlistPath,
const bool getShortID) {
return git::GetVersionInfo(masterlistPath, getShortID);
}
bool IsLatestFile(
const std::filesystem::path& masterlist_path,
const std::string& branch) {
return git::IsLatest(masterlist_path, branch);
}
}
-27
View File
@@ -95,33 +95,6 @@ void ApiDatabase::WriteUserMetadata(const std::filesystem::path& outputFile,
userlist_.Save(outputFile);
}
////////////////////////////////////
// LOOT Functionality Functions
////////////////////////////////////
bool ApiDatabase::UpdateMasterlist(const std::filesystem::path& masterlistPath,
const std::string& remoteURL,
const std::string& remoteBranch) {
if (!std::filesystem::is_directory(masterlistPath.parent_path()))
throw std::invalid_argument("The path \"" +
masterlistPath.u8string() +
"\" does not have a valid parent directory.");
return git::UpdateFile(masterlistPath, remoteURL, remoteBranch);
}
FileRevision ApiDatabase::GetMasterlistRevision(
const std::filesystem::path& masterlistPath,
const bool getShortID) const {
return git::GetVersionInfo(masterlistPath, getShortID);
}
bool ApiDatabase::IsLatestMasterlist(
const std::filesystem::path& masterlist_path,
const std::string& branch) const {
return git::IsLatest(masterlist_path, branch);
}
//////////////////////////
// DB Access Functions
//////////////////////////
-12
View File
@@ -31,7 +31,6 @@
#include "api/game/game_cache.h"
#include "api/game/load_order_handler.h"
#include "api/helpers/git.h"
#include "api/metadata/condition_evaluator.h"
#include "api/metadata_list.h"
#include "loot/database_interface.h"
@@ -52,17 +51,6 @@ struct ApiDatabase : public DatabaseInterface {
void WriteMinimalList(const std::filesystem::path& outputFile,
const bool overwrite) const;
bool UpdateMasterlist(const std::filesystem::path& masterlist_path,
const std::string& remote_url,
const std::string& remote_branch);
FileRevision GetMasterlistRevision(
const std::filesystem::path& masterlist_path,
const bool get_short_id) const;
bool IsLatestMasterlist(const std::filesystem::path& masterlist_path,
const std::string& branch) const;
std::vector<std::string> GetKnownBashTags() const;
std::vector<Message> GetGeneralMessages(
@@ -273,144 +273,6 @@ TEST_P(DatabaseInterfaceTest, writeUserMetadataShouldShouldWriteUserMetadata) {
EXPECT_FALSE(GetFileContent(minimalOutputPath_).empty());
}
TEST_P(DatabaseInterfaceTest,
updateMasterlistShouldThrowIfTheMasterlistPathGivenIsInvalid) {
EXPECT_THROW(db_->UpdateMasterlist("//\?", url_, branch_), std::exception);
}
TEST_P(DatabaseInterfaceTest,
updateMasterlistShouldThrowIfTheMasterlistPathGivenIsEmpty) {
EXPECT_THROW(db_->UpdateMasterlist("", url_, branch_), std::invalid_argument);
}
TEST_P(DatabaseInterfaceTest,
updateMasterlistShouldThrowIfTheRepositoryUrlGivenCannotBeFound) {
EXPECT_THROW(db_->UpdateMasterlist(
masterlistPath,
"https://github.com/loot/oblivion-does-not-exist.git",
branch_),
std::system_error);
}
TEST_P(DatabaseInterfaceTest,
updateMasterlistShouldThrowIfTheRepositoryUrlGivenIsEmpty) {
EXPECT_THROW(db_->UpdateMasterlist(masterlistPath, "", branch_),
std::invalid_argument);
}
TEST_P(DatabaseInterfaceTest,
updateMasterlistShouldThrowIfTheRepositoryBranchGivenCannotBeFound) {
EXPECT_THROW(db_->UpdateMasterlist(masterlistPath, url_, "missing-branch"),
std::system_error);
}
TEST_P(DatabaseInterfaceTest,
updateMasterlistShouldThrowIfTheRepositoryBranchGivenIsEmpty) {
EXPECT_THROW(db_->UpdateMasterlist(masterlistPath, url_, ""),
std::invalid_argument);
}
TEST_P(
DatabaseInterfaceTest,
updateMasterlistShouldSucceedIfPassedValidParametersAndOutputTrueIfTheMasterlistWasUpdated) {
bool updated = false;
EXPECT_NO_THROW(updated =
db_->UpdateMasterlist(masterlistPath, url_, branch_));
EXPECT_TRUE(updated);
EXPECT_TRUE(std::filesystem::exists(masterlistPath));
}
TEST_P(
DatabaseInterfaceTest,
updateMasterlistShouldSucceedIfCalledRepeatedlyButOnlyOutputTrueForTheFirstCall) {
bool updated = false;
EXPECT_NO_THROW(updated =
db_->UpdateMasterlist(masterlistPath, url_, branch_));
EXPECT_TRUE(updated);
EXPECT_NO_THROW(updated =
db_->UpdateMasterlist(masterlistPath, url_, branch_));
EXPECT_FALSE(updated);
EXPECT_TRUE(std::filesystem::exists(masterlistPath));
}
TEST_P(DatabaseInterfaceTest,
getMasterlistRevisionShouldThrowIfNoMasterlistIsPresent) {
FileRevision revision;
EXPECT_THROW(revision = db_->GetMasterlistRevision(masterlistPath, false),
FileAccessError);
EXPECT_TRUE(revision.id.empty());
EXPECT_TRUE(revision.date.empty());
EXPECT_FALSE(revision.is_modified);
}
TEST_P(
DatabaseInterfaceTest,
getMasterlistRevisionShouldThrowIfANonVersionControlledMasterlistIsPresent) {
ASSERT_NO_THROW(GenerateMasterlist());
FileRevision revision;
EXPECT_THROW(revision = db_->GetMasterlistRevision(masterlistPath, false),
GitStateError);
EXPECT_TRUE(revision.id.empty());
EXPECT_TRUE(revision.date.empty());
EXPECT_FALSE(revision.is_modified);
}
TEST_P(
DatabaseInterfaceTest,
getMasterlistRevisionShouldOutputLongStringsAndBooleanFalseIfAVersionControlledMasterlistIsPresentAndGetShortIdParameterIsFalse) {
ASSERT_NO_THROW(db_->UpdateMasterlist(masterlistPath, url_, branch_));
FileRevision revision;
EXPECT_NO_THROW(revision = db_->GetMasterlistRevision(masterlistPath, false));
EXPECT_EQ(40, revision.id.length());
EXPECT_EQ(10, revision.date.length());
EXPECT_FALSE(revision.is_modified);
}
TEST_P(
DatabaseInterfaceTest,
getMasterlistRevisionShouldOutputShortStringsAndBooleanFalseIfAVersionControlledMasterlistIsPresentAndGetShortIdParameterIsTrue) {
ASSERT_NO_THROW(db_->UpdateMasterlist(masterlistPath, url_, branch_));
FileRevision revision;
EXPECT_NO_THROW(revision = db_->GetMasterlistRevision(masterlistPath, false));
EXPECT_GE(size_t(40), revision.id.length());
EXPECT_LE(size_t(7), revision.id.length());
EXPECT_EQ(10, revision.date.length());
EXPECT_FALSE(revision.is_modified);
}
TEST_P(
DatabaseInterfaceTest,
getMasterlistRevisionShouldSucceedIfAnEditedVersionControlledMasterlistIsPresent) {
ASSERT_NO_THROW(db_->UpdateMasterlist(masterlistPath, url_, branch_));
ASSERT_NO_THROW(GenerateMasterlist());
FileRevision revision;
EXPECT_NO_THROW(revision = db_->GetMasterlistRevision(masterlistPath, false));
EXPECT_EQ(40, revision.id.length());
EXPECT_EQ(10, revision.date.length());
EXPECT_TRUE(revision.is_modified);
}
TEST_P(
DatabaseInterfaceTest,
isLatestMasterlistShouldReturnFalseIfTheCurrentRevisionIsNotTheLatestRevisionInTheGivenBranch) {
ASSERT_NO_THROW(db_->UpdateMasterlist(masterlistPath, url_, oldBranch_));
EXPECT_FALSE(db_->IsLatestMasterlist(masterlistPath, branch_));
}
TEST_P(
DatabaseInterfaceTest,
isLatestMasterlistShouldReturnTrueIfTheCurrentRevisionIsTheLatestRevisioninTheGivenBranch) {
ASSERT_NO_THROW(db_->UpdateMasterlist(masterlistPath, url_, branch_));
EXPECT_TRUE(db_->IsLatestMasterlist(masterlistPath, branch_));
}
TEST_P(DatabaseInterfaceTest,
getGroupsShouldReturnAllGroupsListedInTheLoadedMetadata) {
ASSERT_NO_THROW(GenerateMasterlist());
@@ -0,0 +1,196 @@
/* LOOT
A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
Fallout: New Vegas.
Copyright (C) 2014-2016 WrinklyNinja
This file is part of LOOT.
LOOT is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
LOOT is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with LOOT. If not, see
<https://www.gnu.org/licenses/>.
*/
#ifndef LOOT_TESTS_API_INTERFACE_FILE_VERSIONING_TEST
#define LOOT_TESTS_API_INTERFACE_FILE_VERSIONING_TEST
#include "loot/api.h"
#include "tests/api/interface/api_game_operations_test.h"
namespace loot {
namespace test {
class FileVersioningTest : public ApiGameOperationsTest {
protected:
FileVersioningTest() :
url_("./testing-metadata.git"),
branch_("master"),
oldBranch_("old-branch") {}
const std::string url_;
const std::string branch_;
const std::string oldBranch_;
};
// Pass an empty first argument, as it's a prefix for the test instantation,
// but we only have the one so no prefix is necessary.
INSTANTIATE_TEST_SUITE_P(,
FileVersioningTest,
::testing::Values(GameType::tes4,
GameType::tes5,
GameType::fo3,
GameType::fonv,
GameType::fo4,
GameType::tes5se));
TEST_P(FileVersioningTest,
updateFileShouldThrowIfTheMasterlistPathGivenIsInvalid) {
EXPECT_THROW(UpdateFile("//\?", url_, branch_), std::exception);
}
TEST_P(FileVersioningTest,
updateFileShouldThrowIfTheMasterlistPathGivenIsEmpty) {
EXPECT_THROW(UpdateFile("", url_, branch_), std::invalid_argument);
}
TEST_P(FileVersioningTest,
updateFileShouldThrowIfTheRepositoryUrlGivenCannotBeFound) {
EXPECT_THROW(UpdateFile(
masterlistPath,
"https://github.com/loot/oblivion-does-not-exist.git",
branch_),
std::system_error);
}
TEST_P(FileVersioningTest,
updateFileShouldThrowIfTheRepositoryUrlGivenIsEmpty) {
EXPECT_THROW(UpdateFile(masterlistPath, "", branch_),
std::invalid_argument);
}
TEST_P(FileVersioningTest,
updateFileShouldThrowIfTheRepositoryBranchGivenCannotBeFound) {
EXPECT_THROW(UpdateFile(masterlistPath, url_, "missing-branch"),
std::system_error);
}
TEST_P(FileVersioningTest,
updateFileShouldThrowIfTheRepositoryBranchGivenIsEmpty) {
EXPECT_THROW(UpdateFile(masterlistPath, url_, ""),
std::invalid_argument);
}
TEST_P(
FileVersioningTest,
updateFileShouldSucceedIfPassedValidParametersAndOutputTrueIfTheMasterlistWasUpdated) {
bool updated = false;
EXPECT_NO_THROW(updated =
UpdateFile(masterlistPath, url_, branch_));
EXPECT_TRUE(updated);
EXPECT_TRUE(std::filesystem::exists(masterlistPath));
}
TEST_P(
FileVersioningTest,
updateFileShouldSucceedIfCalledRepeatedlyButOnlyOutputTrueForTheFirstCall) {
bool updated = false;
EXPECT_NO_THROW(updated =
UpdateFile(masterlistPath, url_, branch_));
EXPECT_TRUE(updated);
EXPECT_NO_THROW(updated =
UpdateFile(masterlistPath, url_, branch_));
EXPECT_FALSE(updated);
EXPECT_TRUE(std::filesystem::exists(masterlistPath));
}
TEST_P(FileVersioningTest,
getFileRevisionShouldThrowIfNoMasterlistIsPresent) {
FileRevision revision;
EXPECT_THROW(revision = GetFileRevision(masterlistPath, false),
FileAccessError);
EXPECT_TRUE(revision.id.empty());
EXPECT_TRUE(revision.date.empty());
EXPECT_FALSE(revision.is_modified);
}
TEST_P(
FileVersioningTest,
getFileRevisionShouldThrowIfANonVersionControlledMasterlistIsPresent) {
ASSERT_NO_THROW(GenerateMasterlist());
FileRevision revision;
EXPECT_THROW(revision = GetFileRevision(masterlistPath, false),
GitStateError);
EXPECT_TRUE(revision.id.empty());
EXPECT_TRUE(revision.date.empty());
EXPECT_FALSE(revision.is_modified);
}
TEST_P(
FileVersioningTest,
getFileRevisionShouldOutputLongStringsAndBooleanFalseIfAVersionControlledMasterlistIsPresentAndGetShortIdParameterIsFalse) {
ASSERT_NO_THROW(UpdateFile(masterlistPath, url_, branch_));
FileRevision revision;
EXPECT_NO_THROW(revision = GetFileRevision(masterlistPath, false));
EXPECT_EQ(40, revision.id.length());
EXPECT_EQ(10, revision.date.length());
EXPECT_FALSE(revision.is_modified);
}
TEST_P(
FileVersioningTest,
getFileRevisionShouldOutputShortStringsAndBooleanFalseIfAVersionControlledMasterlistIsPresentAndGetShortIdParameterIsTrue) {
ASSERT_NO_THROW(UpdateFile(masterlistPath, url_, branch_));
FileRevision revision;
EXPECT_NO_THROW(revision = GetFileRevision(masterlistPath, false));
EXPECT_GE(size_t(40), revision.id.length());
EXPECT_LE(size_t(7), revision.id.length());
EXPECT_EQ(10, revision.date.length());
EXPECT_FALSE(revision.is_modified);
}
TEST_P(
FileVersioningTest,
getFileRevisionShouldSucceedIfAnEditedVersionControlledMasterlistIsPresent) {
ASSERT_NO_THROW(UpdateFile(masterlistPath, url_, branch_));
ASSERT_NO_THROW(GenerateMasterlist());
FileRevision revision;
EXPECT_NO_THROW(revision = GetFileRevision(masterlistPath, false));
EXPECT_EQ(40, revision.id.length());
EXPECT_EQ(10, revision.date.length());
EXPECT_TRUE(revision.is_modified);
}
TEST_P(
FileVersioningTest,
isLatestFileShouldReturnFalseIfTheCurrentRevisionIsNotTheLatestRevisionInTheGivenBranch) {
ASSERT_NO_THROW(UpdateFile(masterlistPath, url_, oldBranch_));
EXPECT_FALSE(IsLatestFile(masterlistPath, branch_));
}
TEST_P(
FileVersioningTest,
isLatestFileShouldReturnTrueIfTheCurrentRevisionIsTheLatestRevisioninTheGivenBranch) {
ASSERT_NO_THROW(UpdateFile(masterlistPath, url_, branch_));
EXPECT_TRUE(IsLatestFile(masterlistPath, branch_));
}
}
}
#endif
+1
View File
@@ -27,6 +27,7 @@
#include "loot/api.h"
#include "tests/api/interface/create_game_handle_test.h"
#include "tests/api/interface/database_interface_test.h"
#include "tests/api/interface/file_versioning_test.h"
#include "tests/api/interface/game_interface_test.h"
#include "tests/api/interface/is_compatible_test.h"