Переглянути джерело

More Boost.LEAF error handling, and starting some error handling tests

default_compile_flags
vector-of-bool 3 роки тому
джерело
коміт
966a16edad
9 змінених файлів з 203 додано та 33 видалено
  1. +34
    -10
      src/dds/cli/cmd/sdist_create.cpp
  2. +37
    -2
      src/dds/cli/error_handler.cpp
  3. +29
    -11
      src/dds/sdist/package.cpp
  4. +3
    -2
      src/dds/sdist/package.hpp
  5. +49
    -2
      src/dds/util/result.hpp
  6. +20
    -1
      tests/test_sdist.py
  7. +1
    -2
      tools/dds_ci/proc.py
  8. +27
    -0
      tools/dds_ci/testing/error.py
  9. +3
    -3
      tools/dds_ci/testing/fixtures.py

+ 34
- 10
src/dds/cli/cmd/sdist_create.cpp Переглянути файл

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

+ 37
- 2
src/dds/cli/error_handler.cpp Переглянути файл

@@ -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);
}

+ 29
- 11
src/dds/sdist/package.cpp Переглянути файл

@@ -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
- 2
src/dds/sdist/package.hpp Переглянути файл

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

+ 49
- 2
src/dds/util/result.hpp Переглянути файл

@@ -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 {};

+ 20
- 1
tests/test_sdist.py Переглянути файл

@@ -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()

+ 1
- 2
tools/dds_ci/proc.py Переглянути файл

@@ -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)

+ 27
- 0
tools/dds_ci/testing/error.py Переглянути файл

@@ -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')

+ 3
- 3
tools/dds_ci/testing/fixtures.py Переглянути файл

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


Завантаження…
Відмінити
Зберегти