#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 | ||||