reapack

Package manager for REAPER
Log | Files | Refs | Submodules | README | LICENSE

commit fb7e7a72ed65516a1abe35b0b882d3f7778677ad
parent ed02d2d82f4ff7754d0bb68b7e6aa2f9c7879d21
Author: cfillion <cfillion@users.noreply.github.com>
Date:   Fri,  2 Jun 2017 20:29:10 -0400

Merge branch 'api'

Diffstat:
Asrc/api.cpp | 264+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/api.hpp | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.cpp | 46++++++++++++++++++++++++++++++++--------------
Msrc/path.cpp | 36+++++++++++++++++++++++++++++++++---
Msrc/path.hpp | 2++
Msrc/reapack.cpp | 6++++++
Msrc/reapack.hpp | 6+++++-
Msrc/registry.cpp | 28+++++++++++++++++++++++++---
Msrc/registry.hpp | 14++++++++------
Msrc/version.cpp | 7+++++--
Msrc/version.hpp | 2+-
Atest/api.cpp | 39+++++++++++++++++++++++++++++++++++++++
Mtest/path.cpp | 38++++++++++++++++++++++++++++++++++++--
Mtest/registry.cpp | 12+++++++++++-
Mtest/version.cpp | 4++++
15 files changed, 528 insertions(+), 33 deletions(-)

diff --git a/src/api.cpp b/src/api.cpp @@ -0,0 +1,264 @@ +/* ReaPack: Package manager for REAPER + * Copyright (C) 2015-2017 Christian Fillion + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "api.hpp" + +#include <boost/mpl/aux_/preprocessor/token_equal.hpp> +#include <boost/preprocessor.hpp> + +#include <reaper_plugin_functions.h> + +#include "about.hpp" +#include "errors.hpp" +#include "index.hpp" +#include "reapack.hpp" +#include "registry.hpp" +#include "remote.hpp" +#include "transaction.hpp" + +#define API_PREFIX "ReaPack_" + +using namespace API; +using namespace std; + +ReaPack *API::reapack = nullptr; + +struct PackageEntry { + Registry::Entry regEntry; + vector<Registry::File> files; +}; + +static set<PackageEntry *> s_entries; + +APIDef::APIDef(const APIFunc *func) + : m_func(func) +{ + plugin_register(m_func->cKey, m_func->cImpl); + plugin_register(m_func->reascriptKey, m_func->reascriptImpl); + plugin_register(m_func->definitionKey, m_func->definition); +} + +APIDef::~APIDef() +{ + unregister(m_func->cKey, m_func->cImpl); + unregister(m_func->reascriptKey, m_func->reascriptImpl); + unregister(m_func->definitionKey, m_func->definition); +} + +void APIDef::unregister(const char *key, void *ptr) +{ + char buf[255]; + snprintf(buf, sizeof(buf), "-%s", key); + plugin_register(buf, ptr); +} + +#define BOOST_MPL_PP_TOKEN_EQUAL_void(x) x +#define IS_VOID(type) BOOST_MPL_PP_TOKEN_EQUAL(type, void) + +#define ARG_TYPE(arg) BOOST_PP_TUPLE_ELEM(2, 0, arg) +#define ARG_NAME(arg) BOOST_PP_TUPLE_ELEM(2, 1, arg) + +#define ARGS(r, data, i, arg) BOOST_PP_COMMA_IF(i) ARG_TYPE(arg) ARG_NAME(arg) +#define PARAMS(r, data, i, arg) BOOST_PP_COMMA_IF(i) (ARG_TYPE(arg))(intptr_t)argv[i] +#define DEFARGS(r, macro, i, arg) \ + BOOST_PP_EXPR_IF(i, ",") BOOST_PP_STRINGIZE(macro(arg)) + +#define DEFINE_API(type, name, args, help, ...) \ + namespace API_##name { \ + static type cImpl(BOOST_PP_SEQ_FOR_EACH_I(ARGS, _, args)) __VA_ARGS__ \ + static void *reascriptImpl(void **argv, int argc) { \ + BOOST_PP_EXPR_IF(BOOST_PP_NOT(IS_VOID(type)), return (void *)(intptr_t)) \ + cImpl(BOOST_PP_SEQ_FOR_EACH_I(PARAMS, _, args)); \ + BOOST_PP_EXPR_IF(IS_VOID(type), return nullptr;) \ + } \ + static const char *definition = #type "\0" \ + BOOST_PP_SEQ_FOR_EACH_I(DEFARGS, ARG_TYPE, args) "\0" \ + BOOST_PP_SEQ_FOR_EACH_I(DEFARGS, ARG_NAME, args) "\0" help; \ + }; \ + APIFunc API::name = {\ + "API_" API_PREFIX #name, (void *)&API_##name::cImpl, \ + "APIvararg_" API_PREFIX #name, (void *)&API_##name::reascriptImpl, \ + "APIdef_" API_PREFIX #name, (void *)API_##name::definition, \ + } + +DEFINE_API(bool, AboutInstalledPackage, ((PackageEntry*, entry)), R"( + Show the about dialog of the given package entry. + The repository index is downloaded asynchronously if the cached copy doesn't exist or is older than one week. +)", { + if(!s_entries.count(entry)) + return false; + + // the one given by the user may be deleted while we download the idnex + const Registry::Entry entryCopy = entry->regEntry; + + const Remote &repo = reapack->remote(entryCopy.remote); + if(!repo) + return false; + + Transaction *tx = reapack->setupTransaction(); + if(!tx) + return false; + + const vector<Remote> repos = {repo}; + + tx->fetchIndexes(repos); + tx->onFinish([=] { + const auto &indexes = tx->getIndexes(repos); + if(indexes.empty()) + return; + + const Package *pkg = indexes.front()->find(entryCopy.category, entryCopy.package); + if(pkg) + reapack->about()->setDelegate(make_shared<AboutPackageDelegate>(pkg, entryCopy.version)); + }); + tx->runTasks(); + + return true; +}); + +DEFINE_API(bool, AboutRepository, ((const char*, repoName)), R"( + Show the about dialog of the given repository. Returns true if the repository exists in the user configuration. + The repository index is downloaded asynchronously if the cached copy doesn't exist or is older than one week. +)", { + if(const Remote &repo = reapack->remote(repoName)) { + reapack->about(repo); + return true; + } + + return false; +}); + +DEFINE_API(int, CompareVersions, ((const char*, ver1))((const char*, ver2)) + ((char*, errorOut))((int, errorOut_sz)), R"( + Returns 0 if both versions are equal, a positive value if ver1 is higher than ver2 and a negative value otherwise. +)", { + VersionName a, b; + string error; + + b.tryParse(ver2, &error); + a.tryParse(ver1, &error); + + if(errorOut) + snprintf(errorOut, errorOut_sz, "%s", error.c_str()); + + return a.compare(b); +}); + +DEFINE_API(bool, EnumOwnedFiles, ((PackageEntry*, entry))((int, index)) + ((char*, pathOut))((int, pathOut_sz))((int*, sectionsOut))((int*, typeOut)), R"( + Enumerate the files owned by the given package. Returns false when there is no more data. + + sections: 0=not in action list, &1=main, &2=midi editor, &4=midi inline editor + type: see <a href="#ReaPack_GetEntryInfo">ReaPack_GetEntryInfo</a>. +)", { + const size_t i = index; + + if(!s_entries.count(entry) || i >= entry->files.size()) + return false; + + const Registry::File &file = entry->files[i]; + if(pathOut) + snprintf(pathOut, pathOut_sz, "%s", Path::prefixRoot(file.path).join().c_str()); + if(sectionsOut) + *sectionsOut = file.sections; + if(typeOut) + *typeOut = (int)file.type; + + return entry->files.size() > i + 1; +}); + +DEFINE_API(bool, FreeEntry, ((PackageEntry*, entry)), R"( + Free resources allocated for the given package entry. +)", { + if(!s_entries.count(entry)) + return false; + + s_entries.erase(entry); + delete entry; + return true; +}); + +DEFINE_API(bool, GetEntryInfo, ((PackageEntry*, entry)) + ((char*, repoOut))((int, repoOut_sz))((char*, catOut))((int, catOut_sz)) + ((char*, pkgOut))((int, pkgOut_sz))((char*, descOut))((int, descOut_sz)) + ((int*, typeOut))((char*, verOut))((int, verOut_sz)) + ((char*, authorOut))((int, authorOut_sz)) + ((bool*, pinnedOut))((int*, fileCountOut)), R"( + Get the repository name, category, package name, package description, package type, the currently installed version, author name, pinned status and how many files are owned by the given package entry. + + type: 1=script, 2=extension, 3=effect, 4=data, 5=theme, 6=langpack, 7=webinterface +)", { + if(!s_entries.count(entry)) + return false; + + const Registry::Entry &regEntry = entry->regEntry; + + if(repoOut) + snprintf(repoOut, repoOut_sz, "%s", regEntry.remote.c_str()); + if(catOut) + snprintf(catOut, catOut_sz, "%s", regEntry.category.c_str()); + if(pkgOut) + snprintf(pkgOut, pkgOut_sz, "%s", regEntry.package.c_str()); + if(descOut) + snprintf(descOut, descOut_sz, "%s", regEntry.description.c_str()); + if(typeOut) + *typeOut = (int)regEntry.type; + if(verOut) + snprintf(verOut, verOut_sz, "%s", regEntry.version.toString().c_str()); + if(authorOut) + snprintf(authorOut, authorOut_sz, "%s", regEntry.author.c_str()); + if(pinnedOut) + *pinnedOut = regEntry.pinned; + if(fileCountOut) + *fileCountOut = (int)entry->files.size(); + + return true; +}); + +DEFINE_API(PackageEntry*, GetOwner, ((const char*, fn))((char*, errorOut))((int, errorOut_sz)), R"( + Returns the package entry owning the given file. + Delete the returned object from memory after use with <a href="#ReaPack_FreeEntry">ReaPack_FreeEntry</a>. +)", { + Path path(fn); + + const Path &rp = ReaPack::resourcePath(); + + if(path.startsWith(rp)) + path.remove(0, rp.size()); + + try { + const Registry reg(Path::prefixRoot(Path::REGISTRY)); + const auto &owner = reg.getOwner(path); + + if(owner) { + auto entry = new PackageEntry{owner, reg.getFiles(owner)}; + s_entries.insert(entry); + return entry; + } + else if(errorOut) + snprintf(errorOut, errorOut_sz, "the file is not owned by any package entry"); + + return nullptr; + } + catch(const reapack_error &e) + { + if(errorOut) + snprintf(errorOut, errorOut_sz, "%s", e.what()); + + return nullptr; + } +}); diff --git a/src/api.hpp b/src/api.hpp @@ -0,0 +1,57 @@ +/* ReaPack: Package manager for REAPER + * Copyright (C) 2015-2017 Christian Fillion + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef REAPACK_API_HPP +#define REAPACK_API_HPP + +class ReaPack; + +struct APIFunc { + const char *cKey; + void *cImpl; + + const char *reascriptKey; + void *reascriptImpl; + + const char *definitionKey; + void *definition; +}; + +class APIDef { +public: + APIDef(const APIFunc *); + ~APIDef(); + +private: + void unregister(const char *key, void *ptr); + + const APIFunc *m_func; +}; + +namespace API { + extern ReaPack *reapack; + + extern APIFunc AboutInstalledPackage; + extern APIFunc AboutRepository; + extern APIFunc CompareVersions; + extern APIFunc EnumOwnedFiles; + extern APIFunc FreeEntry; + extern APIFunc GetEntryInfo; + extern APIFunc GetOwner; +}; + +#endif diff --git a/src/main.cpp b/src/main.cpp @@ -15,6 +15,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include "api.hpp" #include "errors.hpp" #include "menu.hpp" #include "reapack.hpp" @@ -140,6 +141,34 @@ static bool checkLocation(REAPER_PLUGIN_HINSTANCE module) return false; } +static void setupActions() +{ + reapack->setupAction("REAPACK_SYNC", "ReaPack: Synchronize packages", + &reapack->syncAction, bind(&ReaPack::synchronizeAll, reapack)); + + reapack->setupAction("REAPACK_BROWSE", "ReaPack: Browse packages...", + &reapack->browseAction, bind(&ReaPack::browsePackages, reapack)); + + reapack->setupAction("REAPACK_IMPORT", "ReaPack: Import a repository...", + &reapack->importAction, bind(&ReaPack::importRemote, reapack)); + + reapack->setupAction("REAPACK_MANAGE", "ReaPack: Manage repositories...", + &reapack->configAction, bind(&ReaPack::manageRemotes, reapack)); + + reapack->setupAction("REAPACK_ABOUT", bind(&ReaPack::aboutSelf, reapack)); +} + +static void setupAPI() +{ + reapack->setupAPI(&API::AboutInstalledPackage); + reapack->setupAPI(&API::AboutRepository); + reapack->setupAPI(&API::CompareVersions); + reapack->setupAPI(&API::EnumOwnedFiles); + reapack->setupAPI(&API::FreeEntry); + reapack->setupAPI(&API::GetEntryInfo); + reapack->setupAPI(&API::GetOwner); +} + extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT( REAPER_PLUGIN_HINSTANCE instance, reaper_plugin_info_t *rec) { @@ -161,21 +190,10 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT( if(!checkLocation(instance)) return 0; - reapack = new ReaPack(instance); - - reapack->setupAction("REAPACK_SYNC", "ReaPack: Synchronize packages", - &reapack->syncAction, bind(&ReaPack::synchronizeAll, reapack)); - - reapack->setupAction("REAPACK_BROWSE", "ReaPack: Browse packages...", - &reapack->browseAction, bind(&ReaPack::browsePackages, reapack)); - - reapack->setupAction("REAPACK_IMPORT", "ReaPack: Import a repository...", - &reapack->importAction, bind(&ReaPack::importRemote, reapack)); - - reapack->setupAction("REAPACK_MANAGE", "ReaPack: Manage repositories...", - &reapack->configAction, bind(&ReaPack::manageRemotes, reapack)); + reapack = API::reapack = new ReaPack(instance); - reapack->setupAction("REAPACK_ABOUT", bind(&ReaPack::aboutSelf, reapack)); + setupActions(); + setupAPI(); plugin_register("hookcommand", (void *)commandHook); plugin_register("hookcustommenu", (void *)menuHook); diff --git a/src/path.cpp b/src/path.cpp @@ -105,6 +105,25 @@ void Path::clear() m_parts.clear(); } +void Path::remove(const size_t pos, size_t count) +{ + if(pos > size()) + return; + else if(pos + count > size()) + count = size() - pos; + + auto begin = m_parts.begin(); + advance(begin, pos); + + auto end = begin; + advance(end, count); + + m_parts.erase(begin, end); + + if(!pos && m_absolute) + m_absolute = false; +} + void Path::removeLast() { if(!empty()) @@ -161,6 +180,19 @@ string Path::last() const return m_parts.back(); } +bool Path::startsWith(const Path &o) const +{ + if(size() < o.size() || absolute() != o.absolute()) + return false; + + for(size_t i = 0; i < o.size(); i++) { + if(o[i] != at(i)) + return false; + } + + return true; +} + bool Path::operator==(const Path &o) const { return m_parts == o.m_parts; @@ -207,9 +239,7 @@ const Path &Path::operator+=(const Path &o) const string &Path::at(const size_t index) const { auto it = m_parts.begin(); - - for(size_t i = 0; i < index; i++) - it++; + advance(it, index); return *it; } diff --git a/src/path.hpp b/src/path.hpp @@ -37,6 +37,7 @@ public: void append(const std::string &part, bool traversal = true); void append(const Path &other); + void remove(size_t pos, size_t count); void removeLast(); void clear(); @@ -49,6 +50,7 @@ public: std::string join(const char sep = 0) const; std::string first() const; std::string last() const; + bool startsWith(const Path &) const; bool operator==(const Path &) const; bool operator!=(const Path &) const; diff --git a/src/reapack.cpp b/src/reapack.cpp @@ -18,6 +18,7 @@ #include "reapack.hpp" #include "about.hpp" +#include "api.hpp" #include "browser.hpp" #include "config.hpp" #include "download.hpp" @@ -142,6 +143,11 @@ bool ReaPack::execActions(const int id, const int) return true; } +void ReaPack::setupAPI(const APIFunc *func) +{ + m_api.push_back(std::make_unique<APIDef>(func)); +} + void ReaPack::synchronizeAll() { const vector<Remote> &remotes = m_config->remotes.getEnabled(); diff --git a/src/reapack.hpp b/src/reapack.hpp @@ -19,7 +19,6 @@ #define REAPACK_REAPACK_HPP #include "path.hpp" -#include "registry.hpp" #include <functional> #include <map> @@ -29,12 +28,14 @@ #include <reaper_plugin.h> class About; +class APIDef; class Browser; class Config; class Manager; class Progress; class Remote; class Transaction; +struct APIFunc; class ReaPack { public: @@ -58,6 +59,8 @@ public: gaccel_register_t *action, const ActionCallback &); bool execActions(int id, int); + void setupAPI(const APIFunc *func); + void synchronizeAll(); void setRemoteEnabled(bool enable, const Remote &); void enable(const Remote &r) { setRemoteEnabled(true, r); } @@ -82,6 +85,7 @@ private: void teardownTransaction(); std::map<int, ActionCallback> m_actions; + std::vector<std::unique_ptr<APIDef> > m_api; Config *m_config; Transaction *m_tx; diff --git a/src/registry.cpp b/src/registry.cpp @@ -57,6 +57,10 @@ Registry::Registry(const Path &path) m_forgetEntry = m_db.prepare("DELETE FROM entries WHERE id = ?"); // file queries + m_getOwner = m_db.prepare( + "SELECT e.id, remote, category, package, desc, e.type, version, author, pinned " + "FROM entries e JOIN files f ON f.entry = e.id WHERE f.path = ? LIMIT 1" + ); m_getFiles = m_db.prepare( "SELECT path, main, type FROM files WHERE entry = ? ORDER BY path" ); @@ -260,9 +264,13 @@ auto Registry::getFiles(const Entry &entry) const -> vector<File> m_getFiles->bind(1, entry.id); m_getFiles->exec([&] { - File file{m_getFiles->stringColumn(0)}; - file.sections = static_cast<int>(m_getFiles->intColumn(1)); - file.type = static_cast<Package::Type>(m_getFiles->intColumn(2)); + int col = 0; + + File file{ + m_getFiles->stringColumn(col++), + static_cast<int>(m_getFiles->intColumn(col++)), + static_cast<Package::Type>(m_getFiles->intColumn(col++)), + }; if(!file.type) // < v1.0rc2 file.type = entry.type; @@ -288,6 +296,20 @@ auto Registry::getMainFiles(const Entry &entry) const -> vector<File> return mainFiles; } +auto Registry::getOwner(const Path &path) const -> Entry +{ + Entry entry{}; + + m_getOwner->bind(1, path.join('/')); + + m_getOwner->exec([&] { + fillEntry(m_getOwner, &entry); + return false; + }); + + return entry; +} + void Registry::forget(const Entry &entry) { m_forgetFiles->bind(1, entry.id); diff --git a/src/registry.hpp b/src/registry.hpp @@ -29,7 +29,9 @@ class Registry { public: struct Entry { - int64_t id; + typedef int64_t id_t; + + id_t id; std::string remote; std::string category; std::string package; @@ -54,6 +56,7 @@ public: Registry(const Path &path = {}); Entry getEntry(const Package *) const; + Entry getOwner(const Path &) const; std::vector<Entry> getEntries(const std::string &) const; std::vector<File> getFiles(const Entry &) const; std::vector<File> getMainFiles(const Entry &) const; @@ -77,6 +80,7 @@ private: Statement *m_findEntry; Statement *m_allEntries; Statement *m_forgetEntry; + Statement *m_getOwner; Statement *m_getFiles; Statement *m_insertFile; @@ -86,13 +90,11 @@ private: size_t m_savePoint; }; -namespace std -{ - template<> struct hash<Registry::Entry> - { +namespace std { + template<> struct hash<Registry::Entry> { std::size_t operator()(const Registry::Entry &e) const { - return std::hash<int64_t>()(e.id); + return std::hash<Registry::Entry::id_t>()(e.id); } }; } diff --git a/src/version.cpp b/src/version.cpp @@ -126,13 +126,16 @@ void VersionName::parse(const string &str) m_stable = letters < 1; } -bool VersionName::tryParse(const string &str) +bool VersionName::tryParse(const string &str, string *errorOut) { try { parse(str); return true; } - catch(const reapack_error &) { + catch(const reapack_error &err) { + if(errorOut) + *errorOut = err.what(); + return false; } } diff --git a/src/version.hpp b/src/version.hpp @@ -38,7 +38,7 @@ public: VersionName(const VersionName &); void parse(const std::string &); - bool tryParse(const std::string &); + bool tryParse(const std::string &, std::string *errorOut = nullptr); size_t size() const { return m_segments.size(); } bool isStable() const { return m_stable; } diff --git a/test/api.cpp b/test/api.cpp @@ -0,0 +1,39 @@ +#include <catch.hpp> + +#include "helper/io.hpp" + +#include <api.hpp> + +using namespace std; + +static const char *M = "[api]"; + +TEST_CASE("CompareVersions", M) { + const auto CompareVersions = (int (*)(const char *, const char *, + char *, int))API::CompareVersions.cImpl; + + char error[255] = {}; + + SECTION("equal") { + REQUIRE(CompareVersions("1.0", "1.0", error, sizeof(error)) == 0); + REQUIRE(strcmp(error, "") == 0); + } + + SECTION("lower") { + REQUIRE(CompareVersions("1.0", "2.0", error, sizeof(error)) < 0); + REQUIRE(strcmp(error, "") == 0); + } + + SECTION("higher") { + REQUIRE(CompareVersions("2.0", "1.0", error, sizeof(error)) > 0); + REQUIRE(strcmp(error, "") == 0); + } + + SECTION("invalid") { + REQUIRE(CompareVersions("abc", "def", error, sizeof(error)) == 0); + REQUIRE(strcmp(error, "invalid version name 'abc'") == 0); + } + + SECTION("invalid no error buffer") + CompareVersions("abc", "def", nullptr, 0); // no crash +} diff --git a/test/path.cpp b/test/path.cpp @@ -228,7 +228,7 @@ TEST_CASE("directory traversal", M) { } } -TEST_CASE("append full paths") { +TEST_CASE("append full paths", M) { Path a; a += Path("a/b"); a.append(Path("c/d")); @@ -237,9 +237,43 @@ TEST_CASE("append full paths") { REQUIRE(a == Path("a/b/c/d/e/f")); } -TEST_CASE("temporary path") { +TEST_CASE("temporary path", M) { TempPath a(Path("hello/world")); REQUIRE(a.target() == Path("hello/world")); REQUIRE(a.temp() == Path("hello/world.part")); } + +TEST_CASE("path starts with", M) { + const Path ref("a/b"); + + REQUIRE(ref.startsWith(ref)); + REQUIRE(Path("a/b/c").startsWith(ref)); + REQUIRE_FALSE(Path("/a/b/c").startsWith(ref)); + REQUIRE_FALSE(Path("0/a/b/c").startsWith(ref)); + REQUIRE_FALSE(Path("a").startsWith(ref)); +} + +TEST_CASE("remove path segments", M) { + Path path("/a/b/c/d/e"); + + SECTION("remove from start") { + path.remove(0, 1); + REQUIRE(path == Path("b/c/d/e")); + REQUIRE_FALSE(path.absolute()); + } + + SECTION("remove from middle") { + path.remove(1, 2); + REQUIRE(path == Path("/a/d/e")); +#ifndef _WIN32 + REQUIRE(path.absolute()); +#endif + } + + SECTION("remove past the end") { + path.remove(4, 2); + path.remove(18, 1); + REQUIRE(path == Path("/a/b/c/d")); + } +} diff --git a/test/registry.cpp b/test/registry.cpp @@ -168,7 +168,7 @@ TEST_CASE("get main files", M) { MAKE_PACKAGE Registry reg; - REQUIRE((reg.getMainFiles({})).empty()); + REQUIRE(reg.getMainFiles({}).empty()); Source *main1 = new Source({}, "url", &ver); main1->setSections(Source::MIDIEditorSection); @@ -205,3 +205,13 @@ TEST_CASE("pin registry entry", M) { reg.setPinned(entry, false); REQUIRE_FALSE(reg.getEntry(&pkg).pinned); } + +TEST_CASE("get file owner", M) { + MAKE_PACKAGE + + Registry reg; + REQUIRE_FALSE(reg.getOwner({})); + + const Registry::Entry &entry = reg.push(&ver); + REQUIRE(reg.getOwner(src->targetPath()) == entry); +} diff --git a/test/version.cpp b/test/version.cpp @@ -90,8 +90,12 @@ TEST_CASE("parse version failsafe", M) { SECTION("invalid") { REQUIRE_FALSE(ver.tryParse("hello")); + string error; + REQUIRE_FALSE(ver.tryParse("world", &error)); + REQUIRE(ver.toString().empty()); REQUIRE(ver.size() == 0); + REQUIRE(error == "invalid version name 'world'"); } }