@@ -3,6 +3,8 @@ | |||
#include <dds/error/errors.hpp> | |||
#include <dds/sdist/dist.hpp> | |||
#include <boost/leaf/common.hpp> | |||
#include <boost/leaf/handle_exception.hpp> | |||
#include <fmt/core.h> | |||
namespace dds::cli::cmd { | |||
@@ -15,16 +17,38 @@ int sdist_create(const options& opts) { | |||
.include_apps = 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 |
@@ -6,12 +6,17 @@ | |||
#include <dds/util/result.hpp> | |||
#include <dds/util/signal.hpp> | |||
#include <boost/leaf/common.hpp> | |||
#include <boost/leaf/handle_error.hpp> | |||
#include <boost/leaf/handle_exception.hpp> | |||
#include <boost/leaf/result.hpp> | |||
#include <fmt/ostream.h> | |||
#include <json5/parse_data.hpp> | |||
#include <neo/scope.hpp> | |||
#include <neo/url/parse.hpp> | |||
#include <fstream> | |||
namespace { | |||
template <dds::cli::subcommand Val> | |||
@@ -22,6 +27,17 @@ auto handlers = std::tuple( // | |||
dds_log(error, "Invalid URL '{}': {}", bad_url.value, exc.what()); | |||
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) { | |||
dds_log(error, "{}", exc.value().what()); | |||
dds_log(error, "{}", exc.value().explanation()); | |||
@@ -39,6 +55,25 @@ auto handlers = std::tuple( // | |||
} // namespace | |||
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); | |||
} |
@@ -3,8 +3,10 @@ | |||
#include <dds/dym.hpp> | |||
#include <dds/error/errors.hpp> | |||
#include <dds/util/log.hpp> | |||
#include <dds/util/result.hpp> | |||
#include <dds/util/string.hpp> | |||
#include <boost/leaf/common.hpp> | |||
#include <range/v3/view/split.hpp> | |||
#include <range/v3/view/split_when.hpp> | |||
#include <range/v3/view/transform.hpp> | |||
@@ -108,34 +110,50 @@ package_manifest package_manifest::load_from_file(const fs::path& fpath) { | |||
package_manifest package_manifest::load_from_json5_str(std::string_view content, | |||
std::string_view input_name) { | |||
auto data = json5::parse_data(content); | |||
try { | |||
auto data = json5::parse_data(content); | |||
return parse_json(data, input_name); | |||
} catch (const semester::walk_error& e) { | |||
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 = { | |||
"package.json5", | |||
"package.jsonc", | |||
"package.json", | |||
}; | |||
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; | |||
} | |||
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); | |||
} |
@@ -3,6 +3,7 @@ | |||
#include <dds/deps.hpp> | |||
#include <dds/pkg/id.hpp> | |||
#include <dds/util/fs.hpp> | |||
#include <dds/util/result.hpp> | |||
#include <optional> | |||
#include <string> | |||
@@ -45,8 +46,8 @@ struct package_manifest { | |||
* for a few file candidates and return the result from the first matching. | |||
* 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 |
@@ -2,9 +2,12 @@ | |||
#include <boost/leaf/on_error.hpp> | |||
#include <boost/leaf/result.hpp> | |||
#include <neo/concepts.hpp> | |||
#include <neo/fwd.hpp> | |||
#include <neo/pp.hpp> | |||
#include <exception> | |||
#include <filesystem> | |||
#include <string> | |||
namespace dds { | |||
@@ -12,7 +15,28 @@ namespace dds { | |||
using boost::leaf::current_error; | |||
using boost::leaf::error_id; | |||
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 | |||
@@ -34,17 +58,40 @@ struct e_url_string { | |||
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 | |||
* Boost.LEAF when catching std::system error. | |||
*/ | |||
[[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 | |||
* in-flight error if the current scope is exitted via exception or a bad result<> | |||
*/ | |||
#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 | |||
template <typename T> | |||
struct boost::leaf::is_result_type<dds::result<T>> : std::true_type {}; |
@@ -2,9 +2,11 @@ import pytest | |||
from pathlib import Path | |||
from typing import Tuple | |||
import subprocess | |||
import platform | |||
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() | |||
@@ -76,3 +78,20 @@ def test_import_sdist_stdin(test_sdist: Tuple[Path, Project]) -> None: | |||
# Excluded file will not be in the sdist: | |||
assert not repo_content_path.joinpath('other-file.txt').is_file(), \ | |||
'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() |
@@ -44,5 +44,4 @@ def run(*cmd: CommandLine, cwd: Optional[Pathish] = None, check: bool = False) - | |||
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) |
@@ -0,0 +1,27 @@ | |||
""" | |||
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') |
@@ -157,9 +157,9 @@ class ProjectOpener(): | |||
@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: | |||
opener = ProjectOpener(dds_2, request, worker_id, tmp_path_factory) | |||
opener = ProjectOpener(dds, request, worker_id, tmp_path_factory) | |||
return opener | |||
@@ -179,7 +179,7 @@ def tmp_project(request: FixtureRequest, worker_id: str, project_opener: Project | |||
@pytest.fixture(scope='session') | |||
def dds_2(dds_exe: Path) -> NewDDSWrapper: | |||
def dds(dds_exe: Path) -> NewDDSWrapper: | |||
wr = NewDDSWrapper(dds_exe) | |||
return wr | |||