From 2804cffe3446640f51a2e0c2f2932abd2fce78fb Mon Sep 17 00:00:00 2001 From: bcumming Date: Tue, 24 Sep 2024 07:49:15 +0200 Subject: [PATCH 1/5] refactoring database code for creation of new repo db --- .gitignore | 1 + meson.build | 8 +- src/uenv/repository.cpp | 215 ++++++++++++++++++++++++++++++++-------- src/uenv/repository.h | 16 +-- src/uenv/sqlite.cpp | 106 -------------------- src/uenv/sqlite.h | 79 --------------- subprojects/curl.wrap | 13 +++ 7 files changed, 200 insertions(+), 238 deletions(-) delete mode 100644 src/uenv/sqlite.cpp delete mode 100644 src/uenv/sqlite.h create mode 100644 subprojects/curl.wrap diff --git a/.gitignore b/.gitignore index 068c010..e5641de 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ subprojects/Catch2-* subprojects/CLI11-* +subprojects/curl-* subprojects/fmt-* subprojects/packagecache subprojects/spdlog-* diff --git a/meson.build b/meson.build index 29c747c..0f8d41d 100644 --- a/meson.build +++ b/meson.build @@ -22,6 +22,9 @@ fmt_dep = subproject('fmt', default_options: ['werror=false', 'warning_level=0 json_dep = subproject('nlohmann_json', default_options: ['werror=false', 'warning_level=0']).get_variable('nlohmann_json_dep') spdlog_dep = subproject('spdlog', default_options: ['werror=false', 'warning_level=0','std_format=disabled','external_fmt=enabled']).get_variable('spdlog_dep') sqlite3_dep = subproject('sqlite3', default_options: ['werror=false', 'warning_level=0']).get_variable('sqlite3_dep') +subproject('curl', default_options: ['werror=false', 'warning_level=0']) + +curl_dep = dependency('libcurl', required: true) # the lib dependency is all of the common funtionality shared between the CLI # and the slurm plugin. @@ -42,7 +45,7 @@ lib_inc = include_directories('src') lib_dep = declare_dependency( sources: lib_src, - dependencies: [sqlite3_dep, fmt_dep, spdlog_dep, json_dep], + dependencies: [curl_dep, sqlite3_dep, fmt_dep, spdlog_dep, json_dep], include_directories: lib_inc ) @@ -84,6 +87,3 @@ unit = executable('unit', if uenv_slurm_plugin subdir('src/slurm') endif - -# install the license -#install_data('LICENSE', install_mode : 'rw-r--r--', install_dir : 'license') diff --git a/src/uenv/repository.cpp b/src/uenv/repository.cpp index 062c4fe..5bad5c2 100644 --- a/src/uenv/repository.cpp +++ b/src/uenv/repository.cpp @@ -74,7 +74,7 @@ validate_repo_path(const std::string& path, bool is_absolute, bool exists) { // A thin wrapper around sqlite3* // A shared pointer with a custom destructor that calls the sqlite3 C API // descructor is used to manage the lifetime of the sqlite3* object. The shared -// pointer is used because the sqlite_statement type needs to hold a re +// pointer is used because the statement type needs to hold a re struct sqlite_database { // type definitions using db_ptr_type = std::shared_ptr; @@ -88,23 +88,59 @@ struct sqlite_database { db_ptr_type data; }; -hopefully open_sqlite_db(const fs::path path) { +hopefully open_sqlite_database(const fs::path path, + repo_mode mode) { + using enum repo_mode; + + int flags = mode == readonly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE; sqlite3* db; - if (sqlite3_open_v2(path.string().c_str(), &db, SQLITE_OPEN_READONLY, - NULL) != SQLITE_OK) { + if (sqlite3_open_v2(path.string().c_str(), &db, flags, NULL) != SQLITE_OK) { return unexpected(fmt::format( "internal sqlite3 error opening database file {}", path.string())); } + // double check that the database can be written if in readwrite mode + if (mode == readwrite && sqlite3_db_readonly(db, "main") == 1) { + // close the database before returning an error + sqlite3_close(db); + return unexpected( + fmt::format("the repo {} is read only", path.string())); + } return sqlite_database(db); } -struct sqlite_statement { +struct statement { // type definitions using db_ptr_type = sqlite_database::db_ptr_type; using stmt_ptr_type = std::unique_ptr; + // constructor + statement(std::string text, sqlite_database& db, sqlite3_stmt* statement) + : text(std::move(text)), db(db.data), + wrap(statement, &sqlite3_finalize) { + } + + // perform the statement. + // return false if there was an error + bool step() { + return SQLITE_OK == sqlite3_step(wrap.get()); + } + + // state + + // the text of the statement, e.g. "SELECT * FROM mytable" + const std::string text; + // the underlying database object + db_ptr_type db; + // the underlying statement object + stmt_ptr_type wrap; +}; + +// extend the statement interface for queries +struct query : private statement { + using statement::statement; + struct column { int index; std::string name; @@ -121,37 +157,30 @@ struct sqlite_statement { } }; - // constructors - sqlite_statement(std::string query, sqlite_database& db, - sqlite3_stmt* statement) - : query(std::move(query)), db(db.data), - statement(statement, &sqlite3_finalize) { - } - // functions bool step() { - return SQLITE_ROW == sqlite3_step(statement.get()); + return SQLITE_ROW == sqlite3_step(wrap.get()); } hopefully operator[](int i) const { - if (i >= sqlite3_column_count(statement.get()) || i < 0) { + if (i >= sqlite3_column_count(wrap.get()) || i < 0) { return unexpected(fmt::format("sqlite3_column_count out of range")); } column result; result.index = i; - result.name = sqlite3_column_name(statement.get(), i); - std::string_view type = sqlite3_column_decltype(statement.get(), i); + result.name = sqlite3_column_name(wrap.get(), i); + std::string_view type = sqlite3_column_decltype(wrap.get(), i); if (type == "TEXT") { result.value = std::string(reinterpret_cast( - sqlite3_column_text(statement.get(), i))); + sqlite3_column_text(wrap.get(), i))); } else if (type == "INTEGER") { // sqlite3 stores integers in the database using between 1-8 bytes // (according to the size of the value) but when they are loaded // into memory, it always stores them as a signed 8 byte value. // So always convert from int64. - result.value = static_cast( - sqlite3_column_int64(statement.get(), i)); + result.value = + static_cast(sqlite3_column_int64(wrap.get(), i)); } else { return unexpected(fmt::format("unknown column type {}", type)); } @@ -160,43 +189,74 @@ struct sqlite_statement { } hopefully operator[](const std::string& name) const { - int num_cols = sqlite3_column_count(statement.get()); + int num_cols = sqlite3_column_count(wrap.get()); for (int col = 0; col < num_cols; ++col) { - if (sqlite3_column_name(statement.get(), col) == name) { + if (sqlite3_column_name(wrap.get(), col) == name) { return operator[](col); } } return unexpected(fmt::format( - "sqlite3 statement does not have a column named '{}'", name)); + "sqlite3 query does not have a column named '{}'", name)); } - - // state - const std::string query; - db_ptr_type db; - stmt_ptr_type statement; }; -hopefully create_sqlite_statement(const std::string& query, - sqlite_database& db) { - sqlite3_stmt* statement; +hopefully create_sqlite3_stmt(const std::string& text, + sqlite_database& db) { + sqlite3_stmt* ptr; sqlite3* D = db.data.get(); - int rc = sqlite3_prepare_v2(D, query.c_str(), query.size() + 1, &statement, - nullptr); + int rc = + sqlite3_prepare_v2(D, text.c_str(), text.size() + 1, &ptr, nullptr); if (SQLITE_OK != rc) { return unexpected( fmt::format("unable to create prepared statement:\n{}\n\n{}", sqlite3_errstr(rc), sqlite3_errmsg(D))); } - return sqlite_statement{query, db, statement}; + return ptr; +} + +hopefully create_statement(const std::string& text, + sqlite_database& db) { + auto ptr = create_sqlite3_stmt(text, db); + if (!ptr) { + return unexpected(ptr.error()); + } + + return statement{text, db, *ptr}; +} + +// create a statement and execute it. +// assumes that the statement will return success after one step +// - use a query type for queries that are stepped once for each return row of +// the database +hopefully exec_statement(const std::string& text, sqlite_database& db) { + auto stmnt = create_statement(text, db); + if (!stmnt) { + return unexpected(stmnt.error()); + } + if (!stmnt->step()) { + return unexpected( + fmt::format("error executing the SQL statment {}", text)); + } + return {}; +} + +hopefully create_query(const std::string& text, sqlite_database& db) { + auto ptr = create_sqlite3_stmt(text, db); + if (!ptr) { + return unexpected(ptr.error()); + } + + return query{text, db, *ptr}; } struct repository_impl { - repository_impl(sqlite_database db, fs::path path, fs::path db_path) - : db(std::move(db)), path(std::move(path)), - db_path(std::move(db_path)) { + repository_impl(sqlite_database db, fs::path path, fs::path db_path, + bool readonly = true) + : db(std::move(db)), path(std::move(path)), db_path(std::move(db_path)), + is_readonly(readonly) { } repository_impl(repository_impl&&) = default; sqlite_database db; @@ -205,6 +265,11 @@ struct repository_impl { util::expected, std::string> query(const uenv_label&); + + util::expected exec(const std::string&); + util::expected add_uenv(const uenv_record&); + + const bool is_readonly; }; repository::repository(repository&&) = default; @@ -213,7 +278,8 @@ repository::repository(std::unique_ptr impl) } util::expected -open_repository(const fs::path& repo_path) { +open_repository(const fs::path& repo_path, repo_mode mode) { + using enum repo_mode; auto db_path = repo_path / "index.db"; if (!fs::is_regular_file(db_path)) { return unexpected(fmt::format("the repository is invalid - the index " @@ -222,13 +288,13 @@ open_repository(const fs::path& repo_path) { } // open the sqlite database - auto db = open_sqlite_db(db_path); + auto db = open_sqlite_database(db_path, mode); if (!db) { return unexpected(db.error()); } - return repository( - std::make_unique(std::move(*db), repo_path, db_path)); + return repository(std::make_unique( + std::move(*db), repo_path, db_path, true)); } util::expected, std::string> @@ -258,7 +324,7 @@ repository_impl::query(const uenv_label& label) { } // spdlog::info("running database query\n{}", query); - auto s = create_sqlite_statement(query, db); + auto s = create_query(query, db); if (!s) { return unexpected( fmt::format("creating database query: {}", s.error())); @@ -280,7 +346,7 @@ repository_impl::query(const uenv_label& label) { if (label.only_name()) { // search for an if name could also be an id if (is_sha(*label.name, 16)) { - auto result = create_sqlite_statement( + auto result = create_query( fmt::format("SELECT * FROM records WHERE id = '{}'", uenv_id(*label.name).string()), db); @@ -298,7 +364,7 @@ repository_impl::query(const uenv_label& label) { } // search for a sha if name could also be a sha256 else if (is_sha(*label.name, 64)) { - auto result = create_sqlite_statement( + auto result = create_query( fmt::format("SELECT * FROM records WHERE sha256 = '{}'", sha256(*label.name).string()), db); @@ -324,6 +390,65 @@ repository_impl::query(const uenv_label& label) { return results; } +// schema + +std::vector schema_tables() { + return { + R"(CREATE TABLE images ( + sha256 TEXT PRIMARY KEY CHECK(length(sha256)==64), + id TEXT UNIQUE CHECK(length(id)==16), + date TEXT NOT NULL, + size INTEGER NOT NULL); + )", + R"(CREATE TABLE uenv ( + version_id INTEGER PRIMARY KEY, + system TEXT NOT NULL, + uarch TEXT NOT NULL, + name TEXT NOT NULL, + version TEXT NOT NULL, + UNIQUE (system, uarch, name, version)); + )", + R"(CREATE TABLE tags ( + version_id INTEGER, + tag TEXT NOT NULL, + sha256 TEXT NOT NULL, + PRIMARY KEY (version_id, tag), + FOREIGN KEY (version_id) + REFERENCES uenv (version_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY (sha256) + REFERENCES images (sha256) + ON DELETE CASCADE + ON UPDATE CASCADE); + )", + R"(CREATE VIEW records AS + SELECT + uenv.system AS system, + uenv.uarch AS uarch, + uenv.name AS name, + uenv.version AS version, + tags.tag AS tag, + images.date AS date, + images.size AS size, + tags.sha256 AS sha256, + images.id AS id + FROM tags + INNER JOIN uenv ON uenv.version_id = tags.version_id + INNER JOIN images ON images.sha256 = tags.sha256; + )"}; +} + +// std::string_view schema() { +void schema(sqlite_database& db) { + exec_statement("BEGIN;", db); + exec_statement("PRAGMA foreign_keys=on;", db); + for (const auto& table : schema_tables()) { + exec_statement(table, db); + } + exec_statement("COMMIT;", db); +} + // wrapping the pimpled implementation repository::~repository() = default; @@ -341,4 +466,8 @@ repository::query(const uenv_label& label) { return impl_->query(label); } +bool repository::is_readonly() const { + return impl_->is_readonly; +} + } // namespace uenv diff --git a/src/uenv/repository.h b/src/uenv/repository.h index 3807173..6d7d898 100644 --- a/src/uenv/repository.h +++ b/src/uenv/repository.h @@ -9,9 +9,6 @@ namespace uenv { -// PIMPL forward declaration -struct repository; - /// get the default location for the user's repository. /// - use the environment variable UENV_REPO_PATH if it is set /// - use $SCRATCH/.uenv-images if $SCRATCH is set @@ -22,8 +19,7 @@ util::expected validate_repo_path(const std::string& path, bool is_absolute = true, bool exists = true); -util::expected -open_repository(const std::filesystem::path&); +enum class repo_mode : std::uint8_t { readonly, readwrite }; struct repository_impl; struct repository { @@ -31,6 +27,7 @@ struct repository { std::unique_ptr impl_; public: + using enum repo_mode; repository(repository&&); repository(std::unique_ptr); @@ -44,10 +41,17 @@ struct repository { util::expected, std::string> query(const uenv_label& label); + // return true if the repository is readonly + bool is_readonly() const; + ~repository(); friend util::expected - open_repository(const std::filesystem::path&); + open_repository(const std::filesystem::path&, repo_mode mode); }; +util::expected +open_repository(const std::filesystem::path&, + repo_mode mode = repo_mode::readonly); + } // namespace uenv diff --git a/src/uenv/sqlite.cpp b/src/uenv/sqlite.cpp deleted file mode 100644 index c0c7b3d..0000000 --- a/src/uenv/sqlite.cpp +++ /dev/null @@ -1,106 +0,0 @@ -#include -#include - -#include "sqlite.h" - -std::map sqlite_oflag = { - {sqlite_open::readonly, SQLITE_OPEN_READONLY}}; - -SQLiteDB::SQLiteDB(const std::string& fname, sqlite_open flag) { - int rc = - sqlite3_open_v2(fname.c_str(), &this->db, sqlite_oflag.at(flag), NULL); - if (rc != SQLITE_OK) { - throw SQLiteError("Couldn't open database"); - } -} - -SQLiteDB::~SQLiteDB() { - sqlite3_close(this->db); -} - -/// SQLiteColumn -SQLiteColumn::SQLiteColumn(const SQLiteStatement& statement, int index) - : statement(statement), index(index) { -} - -std::string SQLiteColumn::name() const { - return sqlite3_column_name(this->statement.stmt, this->index); -} - -SQLiteColumn::operator int() const { - auto res = sqlite3_column_type(this->statement.stmt, this->index); - if (res != SQLITE_INTEGER) { - throw SQLiteError("Wrong column type requested"); - } - return sqlite3_column_int(this->statement.stmt, this->index); -} - -SQLiteColumn::operator std::string() const { - const unsigned char* txt = - sqlite3_column_text(this->statement.stmt, this->index); - return reinterpret_cast(txt); -} - -/// SQLiteStatement -SQLiteStatement::SQLiteStatement(SQLiteDB& db, const std::string& query) - : db(db) { - const char* tail; - int rc = - sqlite3_prepare_v2(db.get(), query.c_str(), -1, &this->stmt, &tail); - if (rc != SQLITE_OK) { - throw SQLiteError(sqlite3_errmsg(db.get())); - } - column_count = sqlite3_column_count(this->stmt); -} - -int SQLiteStatement::getColumnIndex(const std::string& name) const { - for (int i = 0; i < this->column_count; ++i) { - if (this->getColumn(i).name() == name) - return i; - } - return -1; -} - -void SQLiteStatement::bind(const std::string& name, const std::string& value) { - int i = sqlite3_bind_parameter_index(this->stmt, name.c_str()); - if (sqlite3_bind_text(this->stmt, i, value.c_str(), -1, SQLITE_STATIC) != - SQLITE_OK) { - throw SQLiteError(std::string("Failed to bind parameter: ") + - sqlite3_errmsg(this->db.get())); - } -} - -void SQLiteStatement::checkIndex(int i) const { - if (i >= this->column_count) { - throw SQLiteError("Column out of range"); - } -} - -std::string SQLiteStatement::getColumnType(int i) const { - checkIndex(i); - const char* result = sqlite3_column_decltype(this->stmt, i); - if (!result) { - throw SQLiteError("Could not determine declared column type."); - } else { - return result; - } -} - -SQLiteColumn SQLiteStatement::getColumn(int i) const { - checkIndex(i); - if (this->rc != SQLITE_ROW) { - throw SQLiteError("Statement invalid"); - } - return SQLiteColumn(*this, i); -} - -bool SQLiteStatement::execute() { - if ((this->rc = sqlite3_step(this->stmt)) == SQLITE_ROW) { - return true; - } - return false; -} - -SQLiteStatement::~SQLiteStatement() { - sqlite3_finalize(this->stmt); -} diff --git a/src/uenv/sqlite.h b/src/uenv/sqlite.h deleted file mode 100644 index baee6bc..0000000 --- a/src/uenv/sqlite.h +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once - -#include -#include - -struct sqlite3_stmt; -struct sqlite3; - -enum class sqlite_open : int { readonly }; - -class SQLiteError : public std::exception { - public: - SQLiteError(const std::string& msg) : msg(msg) { - } - const char* what() const noexcept override { - return msg.c_str(); - } - - private: - std::string msg; -}; - -class SQLiteStatement; - -class SQLiteDB { - public: - SQLiteDB(const std::string& fname, sqlite_open flag); - SQLiteDB(const SQLiteDB&) = delete; - SQLiteDB operator=(const SQLiteDB&) = delete; - virtual ~SQLiteDB(); - - private: - sqlite3* db{nullptr}; - - protected: - sqlite3* get() { - return db; - } - - friend SQLiteStatement; -}; - -class SQLiteColumn; - -class SQLiteStatement { - public: - SQLiteStatement(SQLiteDB& db, const std::string& query); - SQLiteStatement(const SQLiteStatement&) = delete; - SQLiteStatement operator=(const SQLiteStatement&) = delete; - std::string getColumnType(int i) const; - SQLiteColumn getColumn(int i) const; - int getColumnIndex(const std::string& name) const; - void bind(const std::string& name, const std::string& value); - bool execute(); - - virtual ~SQLiteStatement(); - - private: - void checkIndex(int i) const; - - private: - SQLiteDB& db; - sqlite3_stmt* stmt; - int column_count{-1}; - int rc; - friend SQLiteColumn; -}; - -class SQLiteColumn { - public: - SQLiteColumn(const SQLiteStatement& statement, int index); - std::string name() const; - operator int() const; - operator std::string() const; - - private: - const SQLiteStatement& statement; - const int index; -}; diff --git a/subprojects/curl.wrap b/subprojects/curl.wrap new file mode 100644 index 0000000..4472472 --- /dev/null +++ b/subprojects/curl.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = curl-8.10.0 +source_url = https://github.com/curl/curl/releases/download/curl-8_10_0/curl-8.10.0.tar.xz +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/curl_8.10.0-1/curl-8.10.0.tar.xz +source_filename = curl-8.10.0.tar.xz +source_hash = e6b142f0e85e954759d37e26a3627e2278137595be80e3a860c4353e4335e5a0 +patch_filename = curl_8.10.0-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/curl_8.10.0-1/get_patch +patch_hash = 82ff0ee3f852cc8221ee52c2f2b7624098452a219f64f45bb8d5d1b6bf921f0f +wrapdb_version = 8.10.0-1 + +[provide] +dependency_names = libcurl From dd822c3089a8cf702ce5364ba5c7fd555d1c9fef Mon Sep 17 00:00:00 2001 From: bcumming Date: Fri, 27 Sep 2024 19:19:54 +0200 Subject: [PATCH 2/5] testing of repo features --- meson.build | 1 + src/cli/repo.cpp | 96 ++++++++++++++++++ src/cli/repo.h | 34 +++++++ src/cli/uenv.cpp | 11 +- src/cli/uenv.h | 13 ++- src/uenv/repository.cpp | 218 +++++++++++++++++++++++++++++++++------- src/uenv/repository.h | 6 ++ 7 files changed, 341 insertions(+), 38 deletions(-) create mode 100644 src/cli/repo.cpp create mode 100644 src/cli/repo.h diff --git a/meson.build b/meson.build index 0f8d41d..492ae1b 100644 --- a/meson.build +++ b/meson.build @@ -56,6 +56,7 @@ if uenv_cli uenv_src = [ 'src/cli/image.cpp', 'src/cli/ls.cpp', + 'src/cli/repo.cpp', 'src/cli/run.cpp', 'src/cli/start.cpp', 'src/cli/uenv.cpp', diff --git a/src/cli/repo.cpp b/src/cli/repo.cpp new file mode 100644 index 0000000..3eadcea --- /dev/null +++ b/src/cli/repo.cpp @@ -0,0 +1,96 @@ +// vim: ts=4 sts=4 sw=4 et +#include + +#include +#include +#include + +#include +#include +#include + +#include "repo.h" +#include "uenv.h" + +namespace uenv { + +void repo_args::add_cli(CLI::App& cli, + [[maybe_unused]] global_settings& settings) { + auto* repo_cli = + cli.add_subcommand("repo", "manage and query uenv image repositories"); + + // add the create command, i.e. `uenv repo create ...` + auto* create_cli = + repo_cli->add_subcommand("create", "create a new uenv repository"); + + create_cli->add_option("path", create_args.path, + "path of the repo to create"); + create_cli->callback( + [&settings]() { settings.mode = uenv::cli_mode::repo_create; }); + + // add the status command, i.e. `uenv repo status ...` + auto* status_cli = repo_cli->add_subcommand( + "status", "status of an existing uenv repository"); + status_cli->add_option("path", status_args.path, "path of the repo"); + status_cli->callback( + [&settings]() { settings.mode = uenv::cli_mode::repo_status; }); +} + +// inspect the repo path that is optionally passed as an argument. +// if no argument is provided, fall back to the value passed using +// the --repo argument, which in turn falls back to the default value. +util::expected +resolve_repo_path(std::optional path, + const global_settings& settings) { + if (path) { + if (auto result = parse_path(*path); !result) { + return util::unexpected(result.error().message()); + } + return *path; + } + if (settings.repo) { + return *(settings.repo); + } + return util::unexpected("no path provided"); +} + +int repo_create(const repo_create_args& args, const global_settings& settings) { + auto path = resolve_repo_path(args.path, settings); + if (!path) { + spdlog::error("invalid repository path: {}", path.error()); + return 1; + } + spdlog::info("attempting to create uenv repo at {}", *path); + auto x = create_repository(*path); + if (!x) { + spdlog::error("{}", x.error()); + } + return 0; +} + +int repo_status(const repo_status_args& args, const global_settings& settings) { + using enum repo_state; + + auto path = resolve_repo_path(args.path, settings); + if (!path) { + spdlog::error("invalid repository path: {}", path.error()); + return 1; + } + auto status = validate_repository(*path); + if (status == readonly) { + fmt::print("the repository at {} is read only\n", *path); + } + if (status == readwrite) { + fmt::print("the repository at {} is read-write\n", *path); + } + if (status == no_exist) { + fmt::print("no repository at {}\n", *path); + } + if (status == invalid) { + fmt::print("the repository at {} is in invalid state\n", *path); + } + + return 0; +} + +} // namespace uenv diff --git a/src/cli/repo.h b/src/cli/repo.h new file mode 100644 index 0000000..3b75f6e --- /dev/null +++ b/src/cli/repo.h @@ -0,0 +1,34 @@ +// vim: ts=4 sts=4 sw=4 et +#pragma once + +#include +#include + +#include "uenv.h" + +namespace uenv { + +/* +uenv repo create +uenv repo status +*/ + +struct repo_create_args { + std::optional path; +}; +struct repo_status_args { + std::optional path; +}; + +void repo_help(); + +struct repo_args { + repo_create_args create_args; + repo_status_args status_args; + void add_cli(CLI::App&, global_settings& settings); +}; + +int repo_create(const repo_create_args& args, const global_settings& settings); +int repo_status(const repo_status_args& args, const global_settings& settings); + +} // namespace uenv diff --git a/src/cli/uenv.cpp b/src/cli/uenv.cpp index dda3dc5..ad606f9 100644 --- a/src/cli/uenv.cpp +++ b/src/cli/uenv.cpp @@ -13,6 +13,7 @@ #include #include "image.h" +#include "repo.h" #include "run.h" #include "start.h" #include "uenv.h" @@ -30,10 +31,12 @@ int main(int argc, char** argv) { uenv::start_args start; uenv::run_args run; uenv::image_args image; + uenv::repo_args repo; start.add_cli(cli, settings); run.add_cli(cli, settings); image.add_cli(cli, settings); + repo.add_cli(cli, settings); CLI11_PARSE(cli, argc, argv); @@ -58,7 +61,7 @@ int main(int argc, char** argv) { // if a repo was not provided as a flag, look at environment variables if (!settings.repo_) { if (const auto p = uenv::default_repo_path()) { - settings.repo_ = *uenv::default_repo_path(); + settings.repo_ = *p; } else { spdlog::warn("ignoring the default repo path: {}", p.error()); } @@ -78,7 +81,7 @@ int main(int argc, char** argv) { } if (settings.repo) { - spdlog::info("using repo {}", *settings.repo); + spdlog::info("the repo {}", *settings.repo); } spdlog::info("{}", settings); @@ -90,6 +93,10 @@ int main(int argc, char** argv) { return uenv::run(run, settings); case settings.image_ls: return uenv::image_ls(image.ls_args, settings); + case settings.repo_create: + return uenv::repo_create(repo.create_args, settings); + case settings.repo_status: + return uenv::repo_status(repo.status_args, settings); case settings.unset: fmt::println("uenv version {}", UENV_VERSION); fmt::println("call '{} --help' for help", argv[0]); diff --git a/src/cli/uenv.h b/src/cli/uenv.h index c4d9ace..da1bbaf 100644 --- a/src/cli/uenv.h +++ b/src/cli/uenv.h @@ -12,7 +12,14 @@ namespace uenv { -enum class cli_mode : std::uint32_t { unset, start, run, image_ls }; +enum class cli_mode : std::uint32_t { + unset, + start, + run, + image_ls, + repo_create, + repo_status +}; struct global_settings { using enum cli_mode; @@ -51,6 +58,10 @@ template <> class fmt::formatter { return format_to(ctx.out(), "run"); case image_ls: return format_to(ctx.out(), "image-ls"); + case repo_create: + return format_to(ctx.out(), "repo-create"); + case repo_status: + return format_to(ctx.out(), "repo-status"); } return format_to(ctx.out(), "unknown"); } diff --git a/src/uenv/repository.cpp b/src/uenv/repository.cpp index 5bad5c2..aaed708 100644 --- a/src/uenv/repository.cpp +++ b/src/uenv/repository.cpp @@ -34,16 +34,37 @@ using util::unexpected; /// returns error if /// - the provided path is not absolute /// - the path string was not valid +/// +/// returns error if it a path is set, but it is invalid util::expected, std::string> default_repo_path() { - std::string path_string; + std::optional path_string; if (auto p = std::getenv("UENV_REPO_PATH")) { - return p; - } else if (auto p = std::getenv("SCRATCH")) { - return std::string(p) + "/.uenv-images"; - } else if (auto p = std::getenv("HOME")) { - return std::string(p) + "/.uenv/repo"; + spdlog::debug( + fmt::format("default_repo_path: found UENV_REPO_PATH={}", p)); + return path_string = p; + } else { + spdlog::debug("default_repo_path: skipping UENV_REPO_PATH"); + if (auto p = std::getenv("SCRATCH")) { + spdlog::info(fmt::format("default_repo_path: found SCRATCH={}", p)); + path_string = std::string(p) + "/.uenv-images"; + } else { + spdlog::debug("default_repo_path: skipping SCRATCH"); + if (auto p = std::getenv("HOME")) { + spdlog::info( + fmt::format("default_repo_path: found HOME={}", p)); + path_string = std::string(p) + "/.uenv/repo"; + } else { + spdlog::debug("default_repo_path: no default location found"); + } + } } - return std::nullopt; + if (path_string) { + if (auto result = parse_path(*path_string); !result) { + return unexpected(fmt::format("invalid repository path {}", + result.error().message())); + } + } + return path_string; } util::expected @@ -72,9 +93,10 @@ validate_repo_path(const std::string& path, bool is_absolute, bool exists) { } // A thin wrapper around sqlite3* -// A shared pointer with a custom destructor that calls the sqlite3 C API -// descructor is used to manage the lifetime of the sqlite3* object. The shared -// pointer is used because the statement type needs to hold a re +// A shared pointer with a custom destructor that calls the sqlite3 C +// API descructor is used to manage the lifetime of the sqlite3* object. +// The shared pointer is used because the statement type needs to hold a +// re struct sqlite_database { // type definitions using db_ptr_type = std::shared_ptr; @@ -98,7 +120,8 @@ hopefully open_sqlite_database(const fs::path path, return unexpected(fmt::format( "internal sqlite3 error opening database file {}", path.string())); } - // double check that the database can be written if in readwrite mode + // double check that the database can be written if in readwrite + // mode if (mode == readwrite && sqlite3_db_readonly(db, "main") == 1) { // close the database before returning an error sqlite3_close(db); @@ -175,10 +198,10 @@ struct query : private statement { result.value = std::string(reinterpret_cast( sqlite3_column_text(wrap.get(), i))); } else if (type == "INTEGER") { - // sqlite3 stores integers in the database using between 1-8 bytes - // (according to the size of the value) but when they are loaded - // into memory, it always stores them as a signed 8 byte value. - // So always convert from int64. + // sqlite3 stores integers in the database using between 1-8 + // bytes (according to the size of the value) but when they + // are loaded into memory, it always stores them as a signed + // 8 byte value. So always convert from int64. result.value = static_cast(sqlite3_column_int64(wrap.get(), i)); } else { @@ -229,8 +252,8 @@ hopefully create_statement(const std::string& text, // create a statement and execute it. // assumes that the statement will return success after one step -// - use a query type for queries that are stepped once for each return row of -// the database +// - use a query type for queries that are stepped once for each return +// row of the database hopefully exec_statement(const std::string& text, sqlite_database& db) { auto stmnt = create_statement(text, db); if (!stmnt) { @@ -277,17 +300,100 @@ repository::repository(std::unique_ptr impl) : impl_(std::move(impl)) { } +// for determining the level of access to a file or directory +// NOTE: if there is an error, or the file does not exist `none` is +// returned. +enum class file_level { none = 0, readonly = 1, readwrite = 2 }; +file_level file_access_level(const fs::path& path) { + using enum file_level; + std::error_code ec; + auto status = fs::status(path, ec); + + if (ec) { + spdlog::error("file_access_level {} error '{}'", path, ec.message()); + return none; + } + + auto p = status.permissions(); + + // check if the path is readable by the user, group, or others + file_level lvl = none; + constexpr auto pnone = std::filesystem::perms::none; + if ((p & fs::perms::owner_read) != pnone || + (p & fs::perms::group_read) != pnone || + (p & fs::perms::others_read) != pnone) { + spdlog::debug("file_access_level {} can be read", path, ec.message()); + lvl = readonly; + } + // check if the path is writable by the user, group, or others + if ((p & fs::perms::owner_write) != pnone || + (p & fs::perms::group_write) != pnone || + (p & fs::perms::others_write) != pnone) { + spdlog::debug("file_access_level {} can be written", path, + ec.message()); + lvl = readwrite; + } + return lvl; +} + +repo_state validate_repository(const fs::path& repo_path) { + using enum repo_state; + + if (!fs::is_directory(repo_path)) { + spdlog::debug("validate_repository: repository path {} does not exist", + repo_path); + return no_exist; + } + spdlog::debug("validate_repository: repository path {} exists", repo_path); + + auto db_path = repo_path / "index.db"; + if (!fs::is_regular_file(db_path)) { + spdlog::debug("validate_repository: database {} does not exist", + db_path); + // the path exists, but there is no database file + return no_exist; + } + spdlog::debug("validate_repository: database {} exists", db_path); + + const auto level = + std::min(file_access_level(repo_path), file_access_level(db_path)); + spdlog::debug("validate_repository: level {}", static_cast(level)); + switch (level) { + case file_level::none: + return invalid; + case file_level::readonly: + return readonly; + case file_level::readwrite: + return readwrite; + } + + // all cases should be handled above + return invalid; +} + util::expected open_repository(const fs::path& repo_path, repo_mode mode) { using enum repo_mode; - auto db_path = repo_path / "index.db"; - if (!fs::is_regular_file(db_path)) { - return unexpected(fmt::format("the repository is invalid - the index " - "database {} does not exist", - db_path.string())); + const auto initial_state = validate_repository(repo_path); + + switch (initial_state) { + case repo_state::invalid: + return unexpected( + fmt::format("the repository {} is invalid", repo_path)); + case repo_state::no_exist: + return unexpected( + fmt::format("the repository {} does not exist", repo_path)); + case repo_state::readonly: + if (mode == readwrite) { + return unexpected( + fmt::format("the repository {} is read only", repo_path)); + } + default: + break; } // open the sqlite database + auto db_path = repo_path / "index.db"; auto db = open_sqlite_database(db_path, mode); if (!db) { return unexpected(db.error()); @@ -297,6 +403,58 @@ open_repository(const fs::path& repo_path, repo_mode mode) { std::move(*db), repo_path, db_path, true)); } +std::vector schema_tables(); + +util::expected +create_repository(const fs::path& repo_path, bool exists_ok) { + using enum repo_state; + + const auto initial_state = validate_repository(repo_path); + switch (initial_state) { + case invalid: + return unexpected( + fmt::format("the repository {} is invalid", repo_path)); + case readonly: + return unexpected(fmt::format( + "the repository {} already exists and is read only", repo_path)); + case readwrite: + if (exists_ok) { + return open_repository(repo_path, repo_mode::readwrite); + } + return unexpected( + fmt::format("the repository {} already exists", repo_path)); + default: + break; + } + + auto db_path = repo_path / "index.db"; + + // open the sqlite database + auto db = open_sqlite_database(db_path, repo_mode::readwrite); + if (!db) { + return unexpected(fmt::format("unable to create repository {}: {}", + repo_path.string(), db.error())); + } + + if (auto r = exec_statement("BEGIN;", *db); !r) { + return unexpected(r.error()); + } + if (auto r = exec_statement("PRAGMA foreign_keys=on;", *db); !r) { + return unexpected(r.error()); + } + for (const auto& table : schema_tables()) { + if (auto r = exec_statement(table, *db); !r) { + return unexpected(r.error()); + } + } + if (auto r = exec_statement("COMMIT;", *db); !r) { + return unexpected(r.error()); + } + + return repository(std::make_unique( + std::move(*db), repo_path, db_path, true)); +} + util::expected, std::string> repository_impl::query(const uenv_label& label) { std::vector results; @@ -332,9 +490,9 @@ repository_impl::query(const uenv_label& label) { while (s->step()) { // unsafe: unwrap using .value() without checking for errors. // the best way to do this would be to "validate" the database - // beforehand by checking the columns exist. Even better, validate that - // column 0 -> 'system', etc, and use integer indexes to look up more - // effiently. + // beforehand by checking the columns exist. Even better, + // validate that column 0 -> 'system', etc, and use integer + // indexes to look up more effiently. results.push_back({(*s)["system"].value(), (*s)["uarch"].value(), (*s)["name"].value(), (*s)["version"].value(), (*s)["tag"].value(), (*s)["date"].value(), @@ -439,16 +597,6 @@ std::vector schema_tables() { )"}; } -// std::string_view schema() { -void schema(sqlite_database& db) { - exec_statement("BEGIN;", db); - exec_statement("PRAGMA foreign_keys=on;", db); - for (const auto& table : schema_tables()) { - exec_statement(table, db); - } - exec_statement("COMMIT;", db); -} - // wrapping the pimpled implementation repository::~repository() = default; diff --git a/src/uenv/repository.h b/src/uenv/repository.h index 6d7d898..8b5139e 100644 --- a/src/uenv/repository.h +++ b/src/uenv/repository.h @@ -19,6 +19,9 @@ util::expected validate_repo_path(const std::string& path, bool is_absolute = true, bool exists = true); +enum class repo_state { readonly, readwrite, no_exist, invalid }; +repo_state validate_repository(const std::filesystem::path& repo_path); + enum class repo_mode : std::uint8_t { readonly, readwrite }; struct repository_impl; @@ -53,5 +56,8 @@ struct repository { util::expected open_repository(const std::filesystem::path&, repo_mode mode = repo_mode::readonly); +util::expected +create_repository(const std::filesystem::path& repo_path, + bool exists_ok = false); } // namespace uenv From 92f4afea0a6049795b3429930d82113d1565890c Mon Sep 17 00:00:00 2001 From: bcumming Date: Sat, 28 Sep 2024 08:43:56 +0200 Subject: [PATCH 3/5] create repo works --- src/uenv/repository.cpp | 109 +++++++++++++++++++++++++++++----------- src/uenv/repository.h | 3 +- 2 files changed, 80 insertions(+), 32 deletions(-) diff --git a/src/uenv/repository.cpp b/src/uenv/repository.cpp index aaed708..a1c5b28 100644 --- a/src/uenv/repository.cpp +++ b/src/uenv/repository.cpp @@ -106,6 +106,10 @@ struct sqlite_database { } sqlite_database(sqlite_database&&) = default; + std::string error() { + return sqlite3_errmsg(data.get()); + } + // state db_ptr_type data; }; @@ -114,15 +118,25 @@ hopefully open_sqlite_database(const fs::path path, repo_mode mode) { using enum repo_mode; - int flags = mode == readonly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE; + if (!fs::exists(path)) { + return unexpected( + fmt::format("database file {} does not exist", path.string())); + } + + const int flags = + mode == readonly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE; + spdlog::info("open_sqlite_database: attempting to open {} in {} mode.", + path, mode == readonly ? "readonly" : "readwrite"); sqlite3* db; if (sqlite3_open_v2(path.string().c_str(), &db, flags, NULL) != SQLITE_OK) { - return unexpected(fmt::format( - "internal sqlite3 error opening database file {}", path.string())); + return unexpected(fmt::format("did not open database file {}: {}", + path.string(), sqlite3_errmsg(db))); } - // double check that the database can be written if in readwrite - // mode + spdlog::info("open_sqlite_database: opened {}.", path); + + // double check that the database can be written if in readwrite mode if (mode == readwrite && sqlite3_db_readonly(db, "main") == 1) { + spdlog::error("open_sqlite_database: {} was opened read only.", path); // close the database before returning an error sqlite3_close(db); return unexpected( @@ -132,6 +146,30 @@ hopefully open_sqlite_database(const fs::path path, return sqlite_database(db); } +hopefully create_sqlite_database(const fs::path path) { + using enum repo_mode; + + if (fs::exists(path)) { + return unexpected( + fmt::format("database file {} already exists", path.string())); + } + if (!fs::exists(path.parent_path())) { + return unexpected(fmt::format("the path {} needs to be crated first", + path.parent_path().string())); + } + + const int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; + spdlog::info("create_sqlite_database: attempting to open {}"); + sqlite3* db; + if (sqlite3_open_v2(path.string().c_str(), &db, flags, NULL) != SQLITE_OK) { + return unexpected(fmt::format("did not create database file {}: {}", + path.string(), sqlite3_errmsg(db))); + } + spdlog::info("create_sqlite3_database: opened {}.", path); + + return sqlite_database(db); +} + struct statement { // type definitions using db_ptr_type = sqlite_database::db_ptr_type; @@ -147,7 +185,7 @@ struct statement { // perform the statement. // return false if there was an error bool step() { - return SQLITE_OK == sqlite3_step(wrap.get()); + return SQLITE_DONE == sqlite3_step(wrap.get()); } // state @@ -261,7 +299,7 @@ hopefully exec_statement(const std::string& text, sqlite_database& db) { } if (!stmnt->step()) { return unexpected( - fmt::format("error executing the SQL statment {}", text)); + fmt::format("SQL error '{}' executing {}", db.error(), text)); } return {}; } @@ -406,53 +444,64 @@ open_repository(const fs::path& repo_path, repo_mode mode) { std::vector schema_tables(); util::expected -create_repository(const fs::path& repo_path, bool exists_ok) { +create_repository(const fs::path& repo_path) { using enum repo_state; - const auto initial_state = validate_repository(repo_path); + auto abs_repo_path = fs::absolute(repo_path); + + const auto initial_state = validate_repository(abs_repo_path); switch (initial_state) { case invalid: - return unexpected( - fmt::format("the repository {} is invalid", repo_path)); - case readonly: return unexpected(fmt::format( - "the repository {} already exists and is read only", repo_path)); + "unable to create repository: {} is invalid", abs_repo_path)); + case readonly: case readwrite: - if (exists_ok) { - return open_repository(repo_path, repo_mode::readwrite); - } - return unexpected( - fmt::format("the repository {} already exists", repo_path)); + return unexpected(fmt::format( + "unable to create repository: {} already exists", abs_repo_path)); default: break; } - auto db_path = repo_path / "index.db"; + spdlog::debug("creating repo path {}", abs_repo_path); + { + std::error_code ec; + fs::create_directories(abs_repo_path, ec); + if (ec) { + spdlog::error("unable to create repository path: {}", ec.message()); + return unexpected("unable to create repository"); + } + } + + auto db_path = abs_repo_path / "index.db"; // open the sqlite database - auto db = open_sqlite_database(db_path, repo_mode::readwrite); + auto db = create_sqlite_database(db_path); if (!db) { - return unexpected(fmt::format("unable to create repository {}: {}", - repo_path.string(), db.error())); + spdlog::error("unable to create repository database: {}", db.error()); + return unexpected(fmt::format("unable to create repository")); } - if (auto r = exec_statement("BEGIN;", *db); !r) { - return unexpected(r.error()); + if (auto r = exec_statement("BEGIN", *db); !r) { + spdlog::error(r.error()); + return unexpected("unable to create repository"); } - if (auto r = exec_statement("PRAGMA foreign_keys=on;", *db); !r) { - return unexpected(r.error()); + if (auto r = exec_statement("PRAGMA foreign_keys=on", *db); !r) { + spdlog::error(r.error()); + return unexpected("unable to create repository"); } for (const auto& table : schema_tables()) { if (auto r = exec_statement(table, *db); !r) { - return unexpected(r.error()); + spdlog::error(r.error()); + return unexpected("unable to create repository"); } } - if (auto r = exec_statement("COMMIT;", *db); !r) { - return unexpected(r.error()); + if (auto r = exec_statement("COMMIT", *db); !r) { + spdlog::error(r.error()); + return unexpected("unable to create repository"); } return repository(std::make_unique( - std::move(*db), repo_path, db_path, true)); + std::move(*db), abs_repo_path, db_path, true)); } util::expected, std::string> diff --git a/src/uenv/repository.h b/src/uenv/repository.h index 8b5139e..7845bca 100644 --- a/src/uenv/repository.h +++ b/src/uenv/repository.h @@ -57,7 +57,6 @@ util::expected open_repository(const std::filesystem::path&, repo_mode mode = repo_mode::readonly); util::expected -create_repository(const std::filesystem::path& repo_path, - bool exists_ok = false); +create_repository(const std::filesystem::path& repo_path); } // namespace uenv From d9768493f7ecacd231a4a42dd106405634b7af55 Mon Sep 17 00:00:00 2001 From: bcumming Date: Mon, 30 Sep 2024 11:50:17 +0200 Subject: [PATCH 4/5] add tests for the CLI --- src/cli/repo.cpp | 1 + test/integration/cli.bats | 155 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 test/integration/cli.bats diff --git a/src/cli/repo.cpp b/src/cli/repo.cpp index 3eadcea..133e05f 100644 --- a/src/cli/repo.cpp +++ b/src/cli/repo.cpp @@ -64,6 +64,7 @@ int repo_create(const repo_create_args& args, const global_settings& settings) { auto x = create_repository(*path); if (!x) { spdlog::error("{}", x.error()); + return 1; } return 0; } diff --git a/test/integration/cli.bats b/test/integration/cli.bats new file mode 100644 index 0000000..cc075bb --- /dev/null +++ b/test/integration/cli.bats @@ -0,0 +1,155 @@ +function setup() { + bats_install_path=$(realpath ./install) + export BATS_LIB_PATH=$bats_install_path/bats-helpers + + bats_load_library bats-support + bats_load_library bats-assert + load ./common + + export REPOS=$(realpath ../scratch/repos) + export SQFS_LIB=$(realpath ../scratch/sqfs) + + export SRC_PATH=$(realpath ../../) + + export PATH="$(realpath ../../install/bin):$PATH" + + unset UENV_MOUNT_LIST + + # set up location for creation of working repos + export REPO_ROOT=/tmp/uenv-repo + rm -rf $REPO_ROOT + mkdir -p $REPO_ROOT +} + +function teardown() { + : +} + +@test "noargs" { + run uenv + assert_output --partial "uenv version $(cat $SRC_PATH/VERSION)" + assert_success +} + +@test "--version" { + run uenv --version + assert_output "$(cat $SRC_PATH/VERSION)" + assert_success +} + +@test "image ls" { + export UENV_REPO_PATH=$REPOS/apptool + + run uenv image ls + assert_success + assert_line --index 0 --regexp "^uenv\s+arch\s+system\s+id" + assert_output --regexp "app/42.0:v1\s+zen3\s+arapiles" + assert_output --regexp "app/43.0:v1\s+zen3\s+arapiles" + assert_output --regexp "tool/17.3.2:v1\s+zen3\s+arapiles" + + run uenv image ls --no-header + assert_success + refute_line --regexp "^uenv\s+arch\s+system\s+id" + assert_line --regexp "app/42.0:v1\s+zen3\s+arapiles" + assert_line --regexp "app/43.0:v1\s+zen3\s+arapiles" + assert_line --regexp "tool/17.3.2:v1\s+zen3\s+arapiles" + + run uenv image ls --no-header app + assert_success + assert_line --partial "app/42.0:v1" + assert_line --partial "app/43.0:v1" + refute_line --partial "tool/17.3.2:v1" + + run uenv image ls --no-header app/43.0 + assert_success + refute_line --partial "app/42.0:v1" + assert_line --partial "app/43.0:v1" + refute_line --partial "tool/17.3.2:v1" + + run uenv image ls --no-header tool + assert_success + refute_line --partial "app/42.0:v1" + refute_line --partial "app/43.0:v1" + assert_line --partial "tool/17.3.2:v1" + + run uenv image ls wombat + assert_success + assert_output "no matching uenv" + + # empty output if --no-header is used and there are no matches + run uenv image ls wombat --no-header + assert_success + assert_output "" + + # unset the UENV_REPO_PATH variable and use the --repo flag instead + unset UENV_REPO_PATH=$REPOS/apptool + + run uenv --repo=/wombat image ls --no-header + assert_failure + assert_output --partial "the repository /wombat does not exist" + + run uenv --repo=$REPOS/apptool image ls --no-header + assert_success + assert_line --partial "app/42.0:v1" + assert_line --partial "app/43.0:v1" + assert_line --partial "tool/17.3.2:v1" +} + +@test "repo status" { + export RP=$REPOS/apptool + + # + # check the different methods for providing the repo location + # + + # using UENV_REPO_PATH env variable + UENV_REPO_PATH=$RP run uenv repo status + assert_success + assert_line --index 0 "the repository at $RP is read-write" + + # using --repo flag to uenv + run uenv --repo=$RP repo status + assert_success + assert_line --index 0 "the repository at $RP is read-write" + + # as a positional argument to the repo status command itself + run uenv repo status $RP + assert_success + assert_line --index 0 "the repository at $RP is read-write" + + # no error for a path that does not exist + run uenv repo status /wombat + assert_success + assert_line --index 0 "no repository at /wombat" + + # TODO: + # - check a read-only repo + # - check an invalid repo +} + +@test "repo create" { + # using UENV_REPO_PATH env variable + RP=$(mktemp -d $REPO_ROOT/create-XXXXXX) + run uenv repo create $RP + assert_success + assert [ -d $RP ] + assert [ -e $RP/index.db ] + run sqlite3 $RP/index.db .dump + assert_line --partial "CREATE TABLE images" + assert_line --partial "CREATE TABLE uenv" + assert_line --partial "CREATE TABLE tags" + assert_line --partial "CREATE VIEW records AS" + + # create a repo in the same location + # this should be an error + run uenv repo create $RP + assert_failure + assert_line --partial "unable to create repository" + + # try to create a uenv in a read-only path + RP=$REPO_ROOT/ro + mkdir --mode=-w $RP + run uenv repo create $RP/test + assert_failure + assert_line --partial "Permission denied" +} From 1bc52a25e008bff3f48c0850e0d56730235eaf10 Mon Sep 17 00:00:00 2001 From: bcumming Date: Mon, 30 Sep 2024 13:00:40 +0200 Subject: [PATCH 5/5] cli integration tests clean up better --- .gitignore | 2 ++ test/integration/cli.bats | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e5641de..841743b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ subprojects/packagecache subprojects/spdlog-* subprojects/sqlite-amalgamation-* subprojects/nlohmann_json-* + +install diff --git a/test/integration/cli.bats b/test/integration/cli.bats index cc075bb..f550717 100644 --- a/test/integration/cli.bats +++ b/test/integration/cli.bats @@ -22,7 +22,8 @@ function setup() { } function teardown() { - : + export REPO_ROOT=/tmp/uenv-repo + rm -rf $REPO_ROOT } @test "noargs" {