| #include <dds/error/errors.hpp> | #include <dds/error/errors.hpp> | ||||
| #include <dds/sdist/dist.hpp> | #include <dds/sdist/dist.hpp> | ||||
| #include <boost/leaf/common.hpp> | |||||
| #include <boost/leaf/handle_exception.hpp> | |||||
| #include <fmt/core.h> | #include <fmt/core.h> | ||||
| namespace dds::cli::cmd { | namespace dds::cli::cmd { | ||||
| .include_apps = true, | .include_apps = true, | ||||
| .include_tests = true, | .include_tests = true, | ||||
| }; | }; | ||||
| auto pkg_man = package_manifest::load_from_directory(params.project_dir); | |||||
| if (!pkg_man) { | |||||
| dds::throw_user_error< | |||||
| errc::invalid_pkg_filesystem>("The source root at [{}] is not a valid dds source root", | |||||
| params.project_dir.string()); | |||||
| } | |||||
| auto default_filename = fmt::format("{}.tar.gz", pkg_man->id.to_string()); | |||||
| auto filepath = opts.out_path.value_or(fs::current_path() / default_filename); | |||||
| create_sdist_targz(filepath, params); | |||||
| return 0; | |||||
| return boost::leaf::try_catch( | |||||
| [&] { | |||||
| auto pkg_man = package_manifest::load_from_directory(params.project_dir).value(); | |||||
| auto default_filename = fmt::format("{}.tar.gz", pkg_man.id.to_string()); | |||||
| auto filepath = opts.out_path.value_or(fs::current_path() / default_filename); | |||||
| create_sdist_targz(filepath, params); | |||||
| return 0; | |||||
| }, | |||||
| [&](boost::leaf::bad_result, e_missing_file missing, e_human_message msg) { | |||||
| dds_log(error, | |||||
| "A required file is missing for creating a source distribution for [{}]", | |||||
| params.project_dir.string()); | |||||
| dds_log(error, "Error: {}", msg.value); | |||||
| dds_log(error, "Missing file: {}", missing.path.string()); | |||||
| return 1; | |||||
| }, | |||||
| [&](std::error_code ec, e_human_message msg, boost::leaf::e_file_name file) { | |||||
| dds_log(error, "Error: {}", msg.value); | |||||
| dds_log(error, "Failed to access file [{}]: {}", file.value, ec.message()); | |||||
| return 1; | |||||
| }, | |||||
| [&](std::error_code ec, e_human_message msg) { | |||||
| dds_log(error, "Unexpected error: {}: {}", msg.value, ec.message()); | |||||
| return 1; | |||||
| }, | |||||
| [&](boost::leaf::bad_result, std::errc ec) { | |||||
| dds_log(error, | |||||
| "Failed to create source distribution from directory [{}]: {}", | |||||
| params.project_dir.string(), | |||||
| std::generic_category().message(int(ec))); | |||||
| return 1; | |||||
| }); | |||||
| } | } | ||||
| } // namespace dds::cli::cmd | } // namespace dds::cli::cmd |
| #include <dds/util/result.hpp> | #include <dds/util/result.hpp> | ||||
| #include <dds/util/signal.hpp> | #include <dds/util/signal.hpp> | ||||
| #include <boost/leaf/common.hpp> | |||||
| #include <boost/leaf/handle_error.hpp> | #include <boost/leaf/handle_error.hpp> | ||||
| #include <boost/leaf/handle_exception.hpp> | #include <boost/leaf/handle_exception.hpp> | ||||
| #include <boost/leaf/result.hpp> | #include <boost/leaf/result.hpp> | ||||
| #include <fmt/ostream.h> | #include <fmt/ostream.h> | ||||
| #include <json5/parse_data.hpp> | |||||
| #include <neo/scope.hpp> | |||||
| #include <neo/url/parse.hpp> | #include <neo/url/parse.hpp> | ||||
| #include <fstream> | |||||
| namespace { | namespace { | ||||
| template <dds::cli::subcommand Val> | template <dds::cli::subcommand Val> | ||||
| dds_log(error, "Invalid URL '{}': {}", bad_url.value, exc.what()); | dds_log(error, "Invalid URL '{}': {}", bad_url.value, exc.what()); | ||||
| return 1; | return 1; | ||||
| }, | }, | ||||
| [](boost::leaf::catch_<dds::error_base> exc, | |||||
| json5::parse_error parse_err, | |||||
| boost::leaf::e_file_name* maybe_fpath) { | |||||
| dds_log(error, "{}", exc.value().what()); | |||||
| dds_log(error, "Invalid JSON5 was found: {}", parse_err.what()); | |||||
| if (maybe_fpath) { | |||||
| dds_log(error, " (While reading from [{}])", maybe_fpath->value); | |||||
| } | |||||
| dds_log(error, "{}", exc.value().explanation()); | |||||
| return 1; | |||||
| }, | |||||
| [](boost::leaf::catch_<dds::error_base> exc) { | [](boost::leaf::catch_<dds::error_base> exc) { | ||||
| dds_log(error, "{}", exc.value().what()); | dds_log(error, "{}", exc.value().what()); | ||||
| dds_log(error, "{}", exc.value().explanation()); | dds_log(error, "{}", exc.value().explanation()); | ||||
| } // namespace | } // namespace | ||||
| int dds::handle_cli_errors(std::function<int()> fn) noexcept { | int dds::handle_cli_errors(std::function<int()> fn) noexcept { | ||||
| return boost::leaf::try_handle_all([&]() -> boost::leaf::result<int> { return fn(); }, | |||||
| handlers); | |||||
| return boost::leaf::try_catch( | |||||
| [&] { | |||||
| boost::leaf::context<dds::e_error_marker> marker_ctx; | |||||
| marker_ctx.activate(); | |||||
| neo_defer { | |||||
| marker_ctx.deactivate(); | |||||
| marker_ctx.handle_error<void>( | |||||
| boost::leaf::current_error(), | |||||
| [](dds::e_error_marker mark) { | |||||
| dds_log(trace, "[error marker {}]", mark.value); | |||||
| auto efile_path = std::getenv("DDS_WRITE_ERROR_MARKER"); | |||||
| if (efile_path) { | |||||
| std::ofstream outfile{efile_path, std::ios::binary}; | |||||
| fmt::print(outfile, "{}", mark.value); | |||||
| } | |||||
| }, | |||||
| [] {}); | |||||
| }; | |||||
| return fn(); | |||||
| }, | |||||
| handlers); | |||||
| } | } |
| #include <dds/dym.hpp> | #include <dds/dym.hpp> | ||||
| #include <dds/error/errors.hpp> | #include <dds/error/errors.hpp> | ||||
| #include <dds/util/log.hpp> | #include <dds/util/log.hpp> | ||||
| #include <dds/util/result.hpp> | |||||
| #include <dds/util/string.hpp> | #include <dds/util/string.hpp> | ||||
| #include <boost/leaf/common.hpp> | |||||
| #include <range/v3/view/split.hpp> | #include <range/v3/view/split.hpp> | ||||
| #include <range/v3/view/split_when.hpp> | #include <range/v3/view/split_when.hpp> | ||||
| #include <range/v3/view/transform.hpp> | #include <range/v3/view/transform.hpp> | ||||
| package_manifest package_manifest::load_from_json5_str(std::string_view content, | package_manifest package_manifest::load_from_json5_str(std::string_view content, | ||||
| std::string_view input_name) { | std::string_view input_name) { | ||||
| auto data = json5::parse_data(content); | |||||
| try { | try { | ||||
| auto data = json5::parse_data(content); | |||||
| return parse_json(data, input_name); | return parse_json(data, input_name); | ||||
| } catch (const semester::walk_error& e) { | } catch (const semester::walk_error& e) { | ||||
| throw_user_error<errc::invalid_pkg_manifest>(e.what()); | throw_user_error<errc::invalid_pkg_manifest>(e.what()); | ||||
| } catch (const json5::parse_error& err) { | |||||
| BOOST_LEAF_THROW_EXCEPTION(user_error<errc::invalid_pkg_manifest>( | |||||
| "Invalid package manifest JSON5 document"), | |||||
| err, | |||||
| boost::leaf::e_file_name{std::string(input_name)}, | |||||
| DDS_ERROR_MARKER("package-json5-parse-error")); | |||||
| } | } | ||||
| } | } | ||||
| std::optional<fs::path> package_manifest::find_in_directory(path_ref dirpath) { | |||||
| result<fs::path> package_manifest::find_in_directory(path_ref dirpath) { | |||||
| auto cands = { | auto cands = { | ||||
| "package.json5", | "package.json5", | ||||
| "package.jsonc", | "package.jsonc", | ||||
| "package.json", | "package.json", | ||||
| }; | }; | ||||
| for (auto c : cands) { | for (auto c : cands) { | ||||
| auto cand = dirpath / c; | |||||
| if (fs::is_regular_file(cand)) { | |||||
| auto cand = dirpath / c; | |||||
| std::error_code ec; | |||||
| if (fs::is_regular_file(cand, ec)) { | |||||
| return cand; | return cand; | ||||
| } | } | ||||
| if (ec != std::errc::no_such_file_or_directory) { | |||||
| return boost::leaf:: | |||||
| new_error(ec, | |||||
| DDS_E_ARG(e_human_message{ | |||||
| "Failed to check for package manifest in project directory"}), | |||||
| DDS_ERROR_MARKER("failed-package-json5-scan"), | |||||
| DDS_E_ARG(boost::leaf::e_file_name{cand.string()})); | |||||
| } | |||||
| } | } | ||||
| return std::nullopt; | |||||
| return boost::leaf::new_error(std::errc::no_such_file_or_directory, | |||||
| DDS_E_ARG( | |||||
| e_human_message{"Expected to find a package manifest file"}), | |||||
| DDS_E_ARG(e_missing_file{dirpath / "package.json5"}), | |||||
| DDS_ERROR_MARKER("no-package-json5")); | |||||
| } | } | ||||
| std::optional<package_manifest> package_manifest::load_from_directory(path_ref dirpath) { | |||||
| auto found = find_in_directory(dirpath); | |||||
| if (!found.has_value()) { | |||||
| return std::nullopt; | |||||
| } | |||||
| return load_from_file(*found); | |||||
| result<package_manifest> package_manifest::load_from_directory(path_ref dirpath) { | |||||
| BOOST_LEAF_AUTO(found, find_in_directory(dirpath)); | |||||
| return load_from_file(found); | |||||
| } | } |
| #include <dds/deps.hpp> | #include <dds/deps.hpp> | ||||
| #include <dds/pkg/id.hpp> | #include <dds/pkg/id.hpp> | ||||
| #include <dds/util/fs.hpp> | #include <dds/util/fs.hpp> | ||||
| #include <dds/util/result.hpp> | |||||
| #include <optional> | #include <optional> | ||||
| #include <string> | #include <string> | ||||
| * for a few file candidates and return the result from the first matching. | * for a few file candidates and return the result from the first matching. | ||||
| * If none match, it will return nullopt. | * If none match, it will return nullopt. | ||||
| */ | */ | ||||
| static std::optional<fs::path> find_in_directory(path_ref); | |||||
| static std::optional<package_manifest> load_from_directory(path_ref); | |||||
| static result<fs::path> find_in_directory(path_ref); | |||||
| static result<package_manifest> load_from_directory(path_ref); | |||||
| }; | }; | ||||
| } // namespace dds | } // namespace dds |
| #include <boost/leaf/on_error.hpp> | #include <boost/leaf/on_error.hpp> | ||||
| #include <boost/leaf/result.hpp> | #include <boost/leaf/result.hpp> | ||||
| #include <neo/concepts.hpp> | |||||
| #include <neo/fwd.hpp> | |||||
| #include <neo/pp.hpp> | #include <neo/pp.hpp> | ||||
| #include <exception> | #include <exception> | ||||
| #include <filesystem> | |||||
| #include <string> | #include <string> | ||||
| namespace dds { | namespace dds { | ||||
| using boost::leaf::current_error; | using boost::leaf::current_error; | ||||
| using boost::leaf::error_id; | using boost::leaf::error_id; | ||||
| using boost::leaf::new_error; | using boost::leaf::new_error; | ||||
| using boost::leaf::result; | |||||
| template <typename T> | |||||
| class result : public boost::leaf::result<T> { | |||||
| public: | |||||
| using result_base = boost::leaf::result<T>; | |||||
| using result_base::result; | |||||
| template <neo::convertible_to<result_base> Other> | |||||
| result(Other&& oth) noexcept | |||||
| : result_base(static_cast<result_base>(NEO_FWD(oth))) {} | |||||
| constexpr bool has_value() const noexcept { return !!*this; } | |||||
| template <typename U> | |||||
| constexpr T value_or(U&& u) const noexcept { | |||||
| if (has_value()) { | |||||
| return **this; | |||||
| } else { | |||||
| return T(NEO_FWD(u)); | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | /** | ||||
| * @brief Error object representing a captured system_error exception | * @brief Error object representing a captured system_error exception | ||||
| std::string value; | std::string value; | ||||
| }; | }; | ||||
| struct e_human_message { | |||||
| std::string value; | |||||
| }; | |||||
| struct e_missing_file { | |||||
| std::filesystem::path path; | |||||
| }; | |||||
| struct e_error_marker { | |||||
| std::string_view value; | |||||
| }; | |||||
| struct e_parse_error { | |||||
| std::string value; | |||||
| }; | |||||
| /** | /** | ||||
| * @brief Capture currently in-flight special exceptions as new error object. Works around a bug in | * @brief Capture currently in-flight special exceptions as new error object. Works around a bug in | ||||
| * Boost.LEAF when catching std::system error. | * Boost.LEAF when catching std::system error. | ||||
| */ | */ | ||||
| [[noreturn]] void capture_exception(); | [[noreturn]] void capture_exception(); | ||||
| #define DDS_E_ARG(...) ([&] { return __VA_ARGS__; }) | |||||
| #define DDS_ERROR_MARKER(Value) DDS_E_ARG(::dds::e_error_marker{Value}) | |||||
| /** | /** | ||||
| * @brief Generate a leaf::on_error object that loads the given expression into the currently | * @brief Generate a leaf::on_error object that loads the given expression into the currently | ||||
| * in-flight error if the current scope is exitted via exception or a bad result<> | * in-flight error if the current scope is exitted via exception or a bad result<> | ||||
| */ | */ | ||||
| #define DDS_E_SCOPE(...) \ | #define DDS_E_SCOPE(...) \ | ||||
| auto NEO_CONCAT(_err_info_, __LINE__) = boost::leaf::on_error([&] { return __VA_ARGS__; }) | |||||
| auto NEO_CONCAT(_err_info_, __LINE__) = boost::leaf::on_error(DDS_E_ARG(__VA_ARGS__)) | |||||
| } // namespace dds | } // namespace dds | ||||
| template <typename T> | |||||
| struct boost::leaf::is_result_type<dds::result<T>> : std::true_type {}; |
| from pathlib import Path | from pathlib import Path | ||||
| from typing import Tuple | from typing import Tuple | ||||
| import subprocess | import subprocess | ||||
| import platform | |||||
| from dds_ci import proc | from dds_ci import proc | ||||
| from dds_ci.testing import ProjectOpener, Project | |||||
| from dds_ci.dds import DDSWrapper | |||||
| from dds_ci.testing import ProjectOpener, Project, error | |||||
| @pytest.fixture() | @pytest.fixture() | ||||
| # Excluded file will not be in the sdist: | # Excluded file will not be in the sdist: | ||||
| assert not repo_content_path.joinpath('other-file.txt').is_file(), \ | assert not repo_content_path.joinpath('other-file.txt').is_file(), \ | ||||
| 'Non-package content appeared in the package cache' | 'Non-package content appeared in the package cache' | ||||
| def test_sdist_invalid_project(tmp_project: Project) -> None: | |||||
| with error.expect_error_marker('no-package-json5'): | |||||
| tmp_project.sdist_create() | |||||
| @pytest.mark.skipif(platform.system() != 'Linux', reason='We know this fails on Linux') | |||||
| def test_sdist_unreadable_dir(dds: DDSWrapper) -> None: | |||||
| with error.expect_error_marker('failed-package-json5-scan'): | |||||
| dds.run(['sdist', 'create', '--project=/root']) | |||||
| def test_sdist_invalid_json5(tmp_project: Project) -> None: | |||||
| tmp_project.write('package.json5', 'bogus json5') | |||||
| with error.expect_error_marker('package-json5-parse-error'): | |||||
| tmp_project.sdist_create() |
| def check_run(*cmd: CommandLine, cwd: Optional[Pathish] = None) -> ProcessResult: | def check_run(*cmd: CommandLine, cwd: Optional[Pathish] = None) -> ProcessResult: | ||||
| command = list(flatten_cmd(cmd)) | |||||
| return subprocess.run(command, cwd=cwd, check=True) | |||||
| return run(cmd, cwd=cwd, check=True) |
| """ | |||||
| Test utility for error checking | |||||
| """ | |||||
| from contextlib import contextmanager | |||||
| from typing import Iterator | |||||
| import subprocess | |||||
| from pathlib import Path | |||||
| import tempfile | |||||
| import os | |||||
| @contextmanager | |||||
| def expect_error_marker(expect: str) -> Iterator[None]: | |||||
| tdir = Path(tempfile.mkdtemp()) | |||||
| err_file = tdir / 'error' | |||||
| try: | |||||
| os.environ['DDS_WRITE_ERROR_MARKER'] = str(err_file) | |||||
| yield | |||||
| assert False, 'dds subprocess did not raise CallProcessError!' | |||||
| except subprocess.CalledProcessError: | |||||
| assert err_file.exists(), 'No error marker file was generated, but dds exited with an error' | |||||
| marker = err_file.read_text().strip() | |||||
| assert marker == expect, \ | |||||
| f'dds did not produce the expected error (Expected {expect}, got {marker})' | |||||
| finally: | |||||
| os.environ.pop('DDS_WRITE_ERROR_MARKER') |
| @pytest.fixture() | @pytest.fixture() | ||||
| def project_opener(request: FixtureRequest, worker_id: str, dds_2: DDSWrapper, | |||||
| def project_opener(request: FixtureRequest, worker_id: str, dds: DDSWrapper, | |||||
| tmp_path_factory: TempPathFactory) -> ProjectOpener: | tmp_path_factory: TempPathFactory) -> ProjectOpener: | ||||
| opener = ProjectOpener(dds_2, request, worker_id, tmp_path_factory) | |||||
| opener = ProjectOpener(dds, request, worker_id, tmp_path_factory) | |||||
| return opener | return opener | ||||
| @pytest.fixture(scope='session') | @pytest.fixture(scope='session') | ||||
| def dds_2(dds_exe: Path) -> NewDDSWrapper: | |||||
| def dds(dds_exe: Path) -> NewDDSWrapper: | |||||
| wr = NewDDSWrapper(dds_exe) | wr = NewDDSWrapper(dds_exe) | ||||
| return wr | return wr | ||||