| @@ -13,7 +13,7 @@ static int _pkg_repo_ls(const options& opts) { | |||
| auto pkg_db = opts.open_pkg_db(); | |||
| neo::sqlite3::database_ref db = pkg_db.database(); | |||
| auto st = db.prepare("SELECT name, remote_url, db_mtime FROM dds_pkg_remotes"); | |||
| auto st = db.prepare("SELECT name, url, db_mtime FROM dds_pkg_remotes"); | |||
| auto tups = neo::sqlite3::iter_tuples<std::string, std::string, std::optional<std::string>>(st); | |||
| for (auto [name, remote_url, mtime] : tups) { | |||
| fmt::print("Remote '{}':\n", name); | |||
| @@ -0,0 +1,60 @@ | |||
| #include "../options.hpp" | |||
| #include <dds/error/nonesuch.hpp> | |||
| #include <dds/pkg/db.hpp> | |||
| #include <dds/pkg/search.hpp> | |||
| #include <dds/util/result.hpp> | |||
| #include <dds/util/string.hpp> | |||
| #include <boost/leaf/handle_exception.hpp> | |||
| #include <fansi/styled.hpp> | |||
| #include <fmt/format.h> | |||
| #include <range/v3/view/transform.hpp> | |||
| using namespace fansi::literals; | |||
| namespace dds::cli::cmd { | |||
| static int _pkg_search(const options& opts) { | |||
| auto cat = opts.open_pkg_db(); | |||
| auto results = *dds::pkg_search(cat.database(), opts.pkg.search.pattern); | |||
| for (pkg_group_search_result const& found : results.found) { | |||
| fmt::print( | |||
| " Name: .bold[{}]\n" | |||
| "Versions: .bold[{}]\n" | |||
| " From: .bold[{}]\n" | |||
| " .bold[{}]\n\n"_styled, | |||
| found.name, | |||
| joinstr(", ", found.versions | ranges::views::transform(&semver::version::to_string)), | |||
| found.remote_name, | |||
| found.description); | |||
| } | |||
| if (results.found.empty()) { | |||
| dds_log(error, | |||
| "There are no packages that match the given pattern \".bold.red[{}]\""_styled, | |||
| opts.pkg.search.pattern.value_or("*")); | |||
| write_error_marker("pkg-search-no-result"); | |||
| return 1; | |||
| } | |||
| return 0; | |||
| } | |||
| int pkg_search(const options& opts) { | |||
| return boost::leaf::try_catch( | |||
| [&] { | |||
| try { | |||
| return _pkg_search(opts); | |||
| } catch (...) { | |||
| capture_exception(); | |||
| } | |||
| }, | |||
| [](e_nonesuch missing) { | |||
| missing.log_error( | |||
| "There are no packages that match the given pattern \".bold.red[{}]\""_styled); | |||
| write_error_marker("pkg-search-no-result"); | |||
| return 1; | |||
| }); | |||
| } | |||
| } // namespace dds::cli::cmd | |||
| @@ -23,6 +23,7 @@ command pkg_repo_add; | |||
| command pkg_repo_update; | |||
| command pkg_repo_ls; | |||
| command pkg_repo_remove; | |||
| command pkg_search; | |||
| command repoman_add; | |||
| command repoman_import; | |||
| command repoman_init; | |||
| @@ -71,6 +72,8 @@ int dispatch_main(const options& opts) noexcept { | |||
| } | |||
| neo::unreachable(); | |||
| } | |||
| case pkg_subcommand::search: | |||
| return cmd::pkg_search(opts); | |||
| case pkg_subcommand::_none_:; | |||
| } | |||
| neo::unreachable(); | |||
| @@ -6,9 +6,11 @@ | |||
| #include <dds/toolchain/toolchain.hpp> | |||
| #include <debate/enum.hpp> | |||
| #include <fansi/styled.hpp> | |||
| using namespace dds; | |||
| using namespace debate; | |||
| using namespace fansi::literals; | |||
| namespace { | |||
| @@ -254,6 +256,10 @@ struct setup { | |||
| .name = "repo", | |||
| .help = "Manage package repositories", | |||
| })); | |||
| setup_pkg_search_cmd(pkg_group.add_parser({ | |||
| .name = "search", | |||
| .help = "Search for packages available to download", | |||
| })); | |||
| } | |||
| void setup_pkg_get_cmd(argument_parser& pkg_get_cmd) { | |||
| @@ -339,6 +345,18 @@ struct setup { | |||
| = "What to do if any of the named repositories do not exist"; | |||
| } | |||
| void setup_pkg_search_cmd(argument_parser& pkg_repo_search_cmd) noexcept { | |||
| pkg_repo_search_cmd.add_argument({ | |||
| .help = std::string( // | |||
| "A name or glob-style pattern. Only matching packages will be returned. \n" | |||
| "Searching is case-insensitive. Only the .italic[name] will be matched (not the \n" | |||
| "version).\n\nIf this parameter is omitted, the search will return .italic[all] \n" | |||
| "available packages."_styled), | |||
| .valname = "<name-or-pattern>", | |||
| .action = put_into(opts.pkg.search.pattern), | |||
| }); | |||
| } | |||
| void setup_sdist_cmd(argument_parser& sdist_cmd) noexcept { | |||
| auto& sdist_grp = sdist_cmd.add_subparsers({ | |||
| .valname = "<sdist-subcommand>", | |||
| @@ -46,6 +46,7 @@ enum class pkg_subcommand { | |||
| get, | |||
| import, | |||
| repo, | |||
| search, | |||
| }; | |||
| /** | |||
| @@ -214,6 +215,14 @@ struct options { | |||
| /// Package IDs to download | |||
| std::vector<string> pkgs; | |||
| } get; | |||
| /** | |||
| * @brief Parameters for 'dds pkg search' | |||
| */ | |||
| struct { | |||
| /// The search pattern, if provided | |||
| opt_string pattern; | |||
| } search; | |||
| } pkg; | |||
| struct { | |||
| @@ -89,7 +89,7 @@ void migrate_repodb_3(nsql::database& db) { | |||
| CREATE TABLE dds_pkg_remotes ( | |||
| remote_id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
| name TEXT NOT NULL UNIQUE, | |||
| remote_url TEXT NOT NULL, | |||
| url TEXT NOT NULL, | |||
| db_etag TEXT, | |||
| db_mtime TEXT | |||
| ); | |||
| @@ -70,10 +70,10 @@ pkg_remote pkg_remote::connect(std::string_view url_str) { | |||
| void pkg_remote::store(nsql::database_ref db) { | |||
| auto st = db.prepare(R"( | |||
| INSERT INTO dds_pkg_remotes (name, remote_url) | |||
| INSERT INTO dds_pkg_remotes (name, url) | |||
| VALUES (?, ?) | |||
| ON CONFLICT (name) DO | |||
| UPDATE SET remote_url = ?2 | |||
| UPDATE SET url = ?2 | |||
| )"); | |||
| nsql::exec(st, _name, _base_url.to_string()); | |||
| } | |||
| @@ -208,16 +208,16 @@ void pkg_remote::update_pkg_db(nsql::database_ref db, | |||
| void dds::update_all_remotes(nsql::database_ref db) { | |||
| dds_log(info, "Updating catalog from all remotes"); | |||
| auto repos_st = db.prepare("SELECT name, remote_url, db_etag, db_mtime FROM dds_pkg_remotes"); | |||
| auto repos_st = db.prepare("SELECT name, url, db_etag, db_mtime FROM dds_pkg_remotes"); | |||
| auto tups = nsql::iter_tuples<std::string, | |||
| std::string, | |||
| std::optional<std::string>, | |||
| std::optional<std::string>>(repos_st) | |||
| | ranges::to_vector; | |||
| for (const auto& [name, remote_url, etag, db_mtime] : tups) { | |||
| DDS_E_SCOPE(e_url_string{remote_url}); | |||
| pkg_remote repo{name, neo::url::parse(remote_url)}; | |||
| for (const auto& [name, url, etag, db_mtime] : tups) { | |||
| DDS_E_SCOPE(e_url_string{url}); | |||
| pkg_remote repo{name, neo::url::parse(url)}; | |||
| repo.update_pkg_db(db, etag, db_mtime); | |||
| } | |||
| @@ -0,0 +1,76 @@ | |||
| #include "./search.hpp" | |||
| #include <dds/dym.hpp> | |||
| #include <dds/error/nonesuch.hpp> | |||
| #include <dds/error/result.hpp> | |||
| #include <dds/util/log.hpp> | |||
| #include <dds/util/string.hpp> | |||
| #include <neo/sqlite3/database.hpp> | |||
| #include <neo/sqlite3/iter_tuples.hpp> | |||
| #include <range/v3/algorithm/sort.hpp> | |||
| #include <range/v3/range/conversion.hpp> | |||
| #include <range/v3/view/transform.hpp> | |||
| using namespace dds; | |||
| namespace nsql = neo::sqlite3; | |||
| result<pkg_search_results> dds::pkg_search(nsql::database_ref db, | |||
| std::optional<std::string_view> pattern) noexcept { | |||
| auto search_st = db.prepare(R"( | |||
| SELECT pkg.name, | |||
| group_concat(version, ';;'), | |||
| description, | |||
| remote.name, | |||
| remote.url | |||
| FROM dds_pkgs AS pkg | |||
| JOIN dds_pkg_remotes AS remote USING(remote_id) | |||
| WHERE lower(pkg.name) GLOB lower(:pattern) | |||
| GROUP BY pkg.name, remote_id, description | |||
| ORDER BY remote.name, pkg.name | |||
| )"); | |||
| // If no pattern, grab _everything_ | |||
| auto final_pattern = pattern.value_or("*"); | |||
| dds_log(debug, "Searching for packages matching pattern '{}'", final_pattern); | |||
| search_st.bindings()[1] = final_pattern; | |||
| auto rows = nsql::iter_tuples<std::string, std::string, std::string, std::string, std::string>( | |||
| search_st); | |||
| std::vector<pkg_group_search_result> found; | |||
| for (auto [name, versions, desc, remote_name, remote_url] : rows) { | |||
| dds_log(debug, | |||
| "Found: {} with versions {} (Description: {}) from {} [{}]", | |||
| name, | |||
| versions, | |||
| desc, | |||
| remote_name, | |||
| remote_url); | |||
| auto version_strs = split(versions, ";;"); | |||
| auto versions_semver | |||
| = version_strs | ranges::views::transform(&semver::version::parse) | ranges::to_vector; | |||
| ranges::sort(versions_semver); | |||
| found.push_back(pkg_group_search_result{ | |||
| .name = name, | |||
| .versions = versions_semver, | |||
| .description = desc, | |||
| .remote_name = remote_name, | |||
| }); | |||
| } | |||
| if (found.empty()) { | |||
| return boost::leaf::new_error([&] { | |||
| auto names_st = db.prepare("SELECT DISTINCT name from dds_pkgs"); | |||
| auto tups = nsql::iter_tuples<std::string>(names_st); | |||
| auto names_vec = tups | ranges::views::transform([](auto&& row) { | |||
| auto [name] = row; | |||
| return name; | |||
| }) | |||
| | ranges::to_vector; | |||
| auto nearest = dds::did_you_mean(final_pattern, names_vec); | |||
| return e_nonesuch{final_pattern, nearest}; | |||
| }); | |||
| } | |||
| return pkg_search_results{.found = std::move(found)}; | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| #pragma once | |||
| #include <dds/error/result_fwd.hpp> | |||
| #include <semver/version.hpp> | |||
| #include <optional> | |||
| #include <string_view> | |||
| #include <vector> | |||
| namespace neo::sqlite3 { | |||
| class database_ref; | |||
| } // namespace neo::sqlite3 | |||
| namespace dds { | |||
| struct pkg_group_search_result { | |||
| std::string name; | |||
| std::vector<semver::version> versions; | |||
| std::string description; | |||
| std::string remote_name; | |||
| }; | |||
| struct pkg_search_results { | |||
| std::vector<pkg_group_search_result> found; | |||
| }; | |||
| result<pkg_search_results> pkg_search(neo::sqlite3::database_ref db, | |||
| std::optional<std::string_view> query) noexcept; | |||
| } // namespace dds | |||
| @@ -86,6 +86,21 @@ replace(std::vector<std::string> strings, std::string_view key, std::string_view | |||
| return strings; | |||
| } | |||
| template <typename Range> | |||
| inline std::string joinstr(std::string_view joiner, Range&& rng) { | |||
| auto iter = std::begin(rng); | |||
| auto end = std::end(rng); | |||
| std::string ret; | |||
| while (iter != end) { | |||
| ret.append(*iter); | |||
| ++iter; | |||
| if (iter != end) { | |||
| ret.append(joiner); | |||
| } | |||
| } | |||
| return ret; | |||
| } | |||
| } // namespace string_utils | |||
| } // namespace dds | |||
| @@ -2,7 +2,7 @@ import json | |||
| import pytest | |||
| from dds_ci.testing import RepoFixture, Project | |||
| from dds_ci.testing import RepoServer, Project | |||
| SIMPLE_CATALOG = { | |||
| "packages": { | |||
| @@ -21,13 +21,13 @@ SIMPLE_CATALOG = { | |||
| @pytest.fixture() | |||
| def test_repo(http_repo: RepoFixture) -> RepoFixture: | |||
| def test_repo(http_repo: RepoServer) -> RepoServer: | |||
| http_repo.import_json_data(SIMPLE_CATALOG) | |||
| return http_repo | |||
| @pytest.fixture() | |||
| def test_project(tmp_project: Project, test_repo: RepoFixture) -> Project: | |||
| def test_project(tmp_project: Project, test_repo: RepoServer) -> Project: | |||
| tmp_project.dds.repo_add(test_repo.url) | |||
| return tmp_project | |||
| @@ -1,6 +1,9 @@ | |||
| from dds_ci.dds import DDSWrapper | |||
| from dds_ci.testing import Project, RepoFixture, PackageJSON | |||
| from dds_ci.testing import Project, RepoServer, PackageJSON | |||
| from dds_ci.testing.error import expect_error_marker | |||
| from dds_ci.testing.http import HTTPRepoServerFactory, RepoServer | |||
| import pytest | |||
| NEO_SQLITE_PKG_JSON = { | |||
| 'packages': { | |||
| @@ -18,32 +21,52 @@ NEO_SQLITE_PKG_JSON = { | |||
| } | |||
| def test_pkg_get(http_repo: RepoFixture, tmp_project: Project) -> None: | |||
| http_repo.import_json_data(NEO_SQLITE_PKG_JSON) | |||
| tmp_project.dds.repo_add(http_repo.url) | |||
| @pytest.fixture(scope='session') | |||
| def _test_repo(http_repo_factory: HTTPRepoServerFactory) -> RepoServer: | |||
| srv = http_repo_factory('test-pkg-db-repo') | |||
| srv.import_json_data(NEO_SQLITE_PKG_JSON) | |||
| return srv | |||
| def test_pkg_get(_test_repo: RepoServer, tmp_project: Project) -> None: | |||
| _test_repo.import_json_data(NEO_SQLITE_PKG_JSON) | |||
| tmp_project.dds.repo_add(_test_repo.url) | |||
| tmp_project.dds.pkg_get('neo-sqlite3@0.3.0') | |||
| assert tmp_project.root.joinpath('neo-sqlite3@0.3.0').is_dir() | |||
| assert tmp_project.root.joinpath('neo-sqlite3@0.3.0/package.jsonc').is_file() | |||
| def test_pkg_repo(http_repo: RepoFixture, tmp_project: Project) -> None: | |||
| def test_pkg_repo(_test_repo: RepoServer, tmp_project: Project) -> None: | |||
| dds = tmp_project.dds | |||
| dds.repo_add(http_repo.url) | |||
| dds.repo_add(_test_repo.url) | |||
| dds.run(['pkg', 'repo', dds.catalog_path_arg, 'ls']) | |||
| def test_pkg_repo_rm(http_repo: RepoFixture, tmp_project: Project) -> None: | |||
| http_repo.import_json_data(NEO_SQLITE_PKG_JSON) | |||
| def test_pkg_repo_rm(_test_repo: RepoServer, tmp_project: Project) -> None: | |||
| _test_repo.import_json_data(NEO_SQLITE_PKG_JSON) | |||
| dds = tmp_project.dds | |||
| dds.repo_add(http_repo.url) | |||
| dds.repo_add(_test_repo.url) | |||
| # Okay: | |||
| tmp_project.dds.pkg_get('neo-sqlite3@0.3.0') | |||
| # Remove the repo: | |||
| dds.run(['pkg', dds.catalog_path_arg, 'repo', 'ls']) | |||
| dds.repo_remove(http_repo.repo_name) | |||
| dds.repo_remove(_test_repo.repo_name) | |||
| # Cannot double-remove a repo: | |||
| with expect_error_marker('repo-rm-no-such-repo'): | |||
| dds.repo_remove(http_repo.repo_name) | |||
| dds.repo_remove(_test_repo.repo_name) | |||
| # Now, fails: | |||
| with expect_error_marker('pkg-get-no-pkg-id-listing'): | |||
| tmp_project.dds.pkg_get('neo-sqlite3@0.3.0') | |||
| def test_pkg_search(_test_repo: RepoServer, tmp_project: Project) -> None: | |||
| _test_repo.import_json_data(NEO_SQLITE_PKG_JSON) | |||
| dds = tmp_project.dds | |||
| with expect_error_marker('pkg-search-no-result'): | |||
| dds.run(['pkg', dds.catalog_path_arg, 'search']) | |||
| dds.repo_add(_test_repo.url) | |||
| dds.run(['pkg', dds.catalog_path_arg, 'search']) | |||
| dds.run(['pkg', dds.catalog_path_arg, 'search', 'neo-sqlite3']) | |||
| dds.run(['pkg', dds.catalog_path_arg, 'search', 'neo-*']) | |||
| with expect_error_marker('pkg-search-no-result'): | |||
| dds.run(['pkg', dds.catalog_path_arg, 'search', 'nonexistent']) | |||
| @@ -2,7 +2,7 @@ import pytest | |||
| from dds_ci.dds import DDSWrapper | |||
| from dds_ci.testing.fixtures import Project | |||
| from dds_ci.testing.http import RepoFixture | |||
| from dds_ci.testing.http import RepoServer | |||
| from dds_ci.testing.error import expect_error_marker | |||
| from pathlib import Path | |||
| @@ -50,7 +50,7 @@ def test_error_double_remove(tmp_repo: Path, dds: DDSWrapper) -> None: | |||
| dds.run(['repoman', 'remove', tmp_repo, 'neo-fun@0.4.0']) | |||
| def test_pkg_http(http_repo: RepoFixture, tmp_project: Project) -> None: | |||
| def test_pkg_http(http_repo: RepoServer, tmp_project: Project) -> None: | |||
| tmp_project.dds.run([ | |||
| 'repoman', '-ltrace', 'add', http_repo.server.root, 'neo-fun@0.4.0', | |||
| 'https://github.com/vector-of-bool/neo-fun/archive/0.4.0.tar.gz?__dds_strpcmp=1' | |||
| @@ -3,7 +3,7 @@ import platform | |||
| import pytest | |||
| from dds_ci.testing import RepoFixture, Project | |||
| from dds_ci.testing import RepoServer, Project | |||
| from dds_ci import proc, toolchain, paths | |||
| CRYPTOPP_JSON = { | |||
| @@ -51,7 +51,7 @@ int main() { | |||
| @pytest.mark.skipif(platform.system() == 'FreeBSD', reason='This one has trouble running on FreeBSD') | |||
| def test_get_build_use_cryptopp(test_parent_dir: Path, tmp_project: Project, http_repo: RepoFixture) -> None: | |||
| def test_get_build_use_cryptopp(test_parent_dir: Path, tmp_project: Project, http_repo: RepoServer) -> None: | |||
| http_repo.import_json_data(CRYPTOPP_JSON) | |||
| tmp_project.dds.repo_add(http_repo.url) | |||
| tmp_project.package_json = { | |||
| @@ -1,10 +1,10 @@ | |||
| from pathlib import Path | |||
| from dds_ci.testing import RepoFixture, ProjectOpener | |||
| from dds_ci.testing import RepoServer, ProjectOpener | |||
| from dds_ci import proc, paths, toolchain | |||
| def test_get_build_use_spdlog(test_parent_dir: Path, project_opener: ProjectOpener, http_repo: RepoFixture) -> None: | |||
| def test_get_build_use_spdlog(test_parent_dir: Path, project_opener: ProjectOpener, http_repo: RepoServer) -> None: | |||
| proj = project_opener.open('project') | |||
| http_repo.import_json_file(proj.root / 'catalog.json') | |||
| proj.dds.repo_add(http_repo.url) | |||
| @@ -1,10 +1,10 @@ | |||
| from .fixtures import Project, ProjectOpener, PackageJSON, LibraryJSON | |||
| from .http import RepoFixture | |||
| from .http import RepoServer | |||
| __all__ = ( | |||
| 'Project', | |||
| 'ProjectOpener', | |||
| 'PackageJSON', | |||
| 'LibraryJSON', | |||
| 'RepoFixture', | |||
| 'RepoServer', | |||
| ) | |||
| @@ -1,8 +1,9 @@ | |||
| from pathlib import Path | |||
| from contextlib import contextmanager | |||
| import socket | |||
| from contextlib import contextmanager, ExitStack, closing | |||
| import json | |||
| from http.server import SimpleHTTPRequestHandler, HTTPServer | |||
| from typing import NamedTuple, Any, Iterator | |||
| from typing import NamedTuple, Any, Iterator, Callable | |||
| from concurrent.futures import ThreadPoolExecutor | |||
| from functools import partial | |||
| import tempfile | |||
| @@ -11,6 +12,16 @@ import subprocess | |||
| import pytest | |||
| from _pytest.fixtures import FixtureRequest | |||
| from _pytest.tmpdir import TempPathFactory | |||
| from dds_ci.dds import DDSWrapper | |||
| def _unused_tcp_port() -> int: | |||
| """Find an unused localhost TCP port from 1024-65535 and return it.""" | |||
| with closing(socket.socket()) as sock: | |||
| sock.bind(('127.0.0.1', 0)) | |||
| return sock.getsockname()[1] | |||
| class DirectoryServingHTTPRequestHandler(SimpleHTTPRequestHandler): | |||
| @@ -54,17 +65,24 @@ def run_http_server(dirpath: Path, port: int) -> Iterator[ServerInfo]: | |||
| httpd.shutdown() | |||
| @pytest.fixture() | |||
| def http_tmp_dir_server(tmp_path: Path, unused_tcp_port: int) -> Iterator[ServerInfo]: | |||
| HTTPServerFactory = Callable[[Path], ServerInfo] | |||
| @pytest.fixture(scope='session') | |||
| def http_server_factory(request: FixtureRequest) -> HTTPServerFactory: | |||
| """ | |||
| Creates an HTTP server that serves the contents of a new | |||
| temporary directory. | |||
| Spawn an HTTP server that serves the content of a directory. | |||
| """ | |||
| with run_http_server(tmp_path, unused_tcp_port) as s: | |||
| yield s | |||
| def _make(p: Path) -> ServerInfo: | |||
| st = ExitStack() | |||
| server = st.enter_context(run_http_server(p, _unused_tcp_port())) | |||
| request.addfinalizer(st.pop_all) | |||
| return server | |||
| return _make | |||
| class RepoFixture: | |||
| class RepoServer: | |||
| """ | |||
| A fixture handle to a dds HTTP repository, including a path and URL. | |||
| """ | |||
| @@ -98,12 +116,40 @@ class RepoFixture: | |||
| ]) | |||
| RepoFactory = Callable[[str], Path] | |||
| @pytest.fixture(scope='session') | |||
| def repo_factory(tmp_path_factory: TempPathFactory, dds: DDSWrapper) -> RepoFactory: | |||
| def _make(name: str) -> Path: | |||
| tmpdir = tmp_path_factory.mktemp('test-repo-') | |||
| dds.run(['repoman', 'init', tmpdir, f'--name={name}']) | |||
| return tmpdir | |||
| return _make | |||
| HTTPRepoServerFactory = Callable[[str], RepoServer] | |||
| @pytest.fixture(scope='session') | |||
| def http_repo_factory(dds_exe: Path, repo_factory: RepoFactory, | |||
| http_server_factory: HTTPServerFactory) -> HTTPRepoServerFactory: | |||
| """ | |||
| Fixture factory that creates new repositories with an HTTP server for them. | |||
| """ | |||
| def _make(name: str) -> RepoServer: | |||
| repo_dir = repo_factory(name) | |||
| server = http_server_factory(repo_dir) | |||
| return RepoServer(dds_exe, server, name) | |||
| return _make | |||
| @pytest.fixture() | |||
| def http_repo(dds_exe: Path, http_tmp_dir_server: ServerInfo, request: FixtureRequest) -> Iterator[RepoFixture]: | |||
| def http_repo(http_repo_factory: HTTPRepoServerFactory, request: FixtureRequest) -> RepoServer: | |||
| """ | |||
| Fixture that creates a new empty dds repository and an HTTP server to serve | |||
| it. | |||
| """ | |||
| name = f'test-repo-{request.function.__name__}' | |||
| subprocess.check_call([str(dds_exe), 'repoman', 'init', str(http_tmp_dir_server.root), f'--name={name}']) | |||
| yield RepoFixture(dds_exe, http_tmp_dir_server, repo_name=name) | |||
| return http_repo_factory(f'test-repo-{request.function.__name__}') | |||