Commit fd95c88a authored by Kirill Smelkov's avatar Kirill Smelkov

golang, errors, fmt: Error chaining (C++/Pyx)

Following errors model in Go, let's add support for errors to wrap other
errors and to be inspected/unwrapped:

- an error can additionally provide way to unwrap itself, if it
  implements errorWrapper interface;
- errors.Unwrap(err) tries to extract wrapped error;
- errors.Is(err) tests whether an item in error's chain matches target;
- `fmt.errorf("... : %w", ... err)` is similar to `fmt.errorf("... : %s", ... err.c_str())`
  but resulting error, when unwrapped, will return err.

Add C++ implementation for the above + tests.
Python analogs will follow in the next patches.

Top-level documentation is TODO.

See https://blog.golang.org/go1.13-errors for error chaining overview.
parent 58fcdd87
/_context.cpp
/_cxx_test.cpp
/_errors.cpp
/_errors_test.cpp
/_fmt.cpp
/_fmt_test.cpp
/_golang.cpp
/_golang_test.cpp
......
# cython: language_level=2
# Copyright (C) 2019 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
# Copyright (C) 2019-2020 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
......@@ -20,11 +20,16 @@
"""Package errors mirrors Go package errors.
- `New` creates new error with provided text.
- `Unwrap` tries to extract wrapped error.
- `Is` tests whether an item in error's chain matches target.
See also https://golang.org/pkg/errors for Go errors package documentation.
See also https://blog.golang.org/go1.13-errors for error chaining overview.
"""
from golang cimport error, string
from golang cimport error, string, cbool
cdef extern from "golang/errors.h" namespace "golang::errors" nogil:
error New(const string& text)
error Unwrap(error err)
cbool Is(error err, error target)
......@@ -30,9 +30,19 @@ from golang cimport topyexc
cdef extern from * nogil:
"""
extern void _test_errors_new_cpp();
extern void _test_errors_unwrap_cpp();
extern void _test_errors_is_cpp();
"""
void _test_errors_new_cpp() except +topyexc
void _test_errors_unwrap_cpp() except +topyexc
void _test_errors_is_cpp() except +topyexc
def test_errors_new_cpp():
with nogil:
_test_errors_new_cpp()
def test_errors_unwrap_cpp():
with nogil:
_test_errors_unwrap_cpp()
def test_errors_is_cpp():
with nogil:
_test_errors_is_cpp()
# cython: language_level=2
# Copyright (C) 2019 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
# Copyright (C) 2019-2020 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
......@@ -22,9 +22,12 @@
- `sprintf` formats text into string.
- `errorf` formats text into error.
NOTE: formatting rules are those of libc, not Go.
NOTE: with exception of %w, formatting rules are those of libc, not Go(*).
See also https://golang.org/pkg/fmt for Go fmt package documentation.
(*) errorf additionally handles Go-like %w to wrap an error similarly to
https://blog.golang.org/go1.13-errors .
"""
from golang cimport string, error
......
......@@ -173,6 +173,15 @@ cdef extern from "golang/libgolang.h" namespace "golang" nogil:
string Error "_ptr()->Error" ()
# error wrapper interface
cppclass _errorWrapper (_error):
error Unwrap()
cppclass errorWrapper (refptr[_errorWrapper]):
# errorWrapper.X = errorWrapper->X in C++
error Unwrap "_ptr()->Unwrap" ()
# ---- python bits ----
cdef void topyexc() except *
......
......@@ -35,6 +35,9 @@ using namespace golang;
// string -> string (not in STL, huh ?!)
string to_string(const string& s) { return s; }
// error -> string
string to_string(error err) { return (err == nil) ? "nil" : err->Error(); }
// vector<T> -> string
template<typename T>
string to_string(const vector<T>& v) {
......
// Copyright (C) 2019 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
// Copyright (C) 2019-2020 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
......@@ -51,4 +51,32 @@ error New(const string& text) {
return adoptref(static_cast<_error*>(new _TextError(text)));
}
error Unwrap(error err) {
if (err == nil)
return nil;
_errorWrapper* _werr = dynamic_cast<_errorWrapper*>(err._ptr());
if (_werr == nil)
return nil;
return _werr->Unwrap();
}
bool Is(error err, error target) {
if (target == nil)
return (err == nil);
for(;;) {
if (err == nil)
return false;
if (typeid(*err) == typeid(*target))
if (err->Error() == target->Error()) // XXX hack instead of dynamic == (not available in C++)
return true;
err = Unwrap(err);
}
}
}} // golang::errors::
#ifndef _NXD_LIBGOLANG_ERRORS_H
#define _NXD_LIBGOLANG_ERRORS_H
// Copyright (C) 2019 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
// Copyright (C) 2019-2020 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
......@@ -23,8 +23,11 @@
// Package errors mirrors Go package errors.
//
// - `New` creates new error with provided text.
// - `Unwrap` tries to extract wrapped error.
// - `Is` tests whether an item in error's chain matches target.
//
// See also https://golang.org/pkg/errors for Go errors package documentation.
// See also https://blog.golang.org/go1.13-errors for error chaining overview.
#include <golang/libgolang.h>
......@@ -35,6 +38,15 @@ namespace errors {
// New creates new error with provided text.
LIBGOLANG_API error New(const string& text);
// Unwrap tries to unwrap error.
//
// If err implements Unwrap method, it returns err.Unwrap().
// Otherwise it returns nil.
LIBGOLANG_API error Unwrap(error err);
// Is returns whether target matches any error in err's error chain.
LIBGOLANG_API bool Is(error err, error target);
}} // golang::errors::
#endif // _NXD_LIBGOLANG_ERRORS_H
......@@ -26,3 +26,84 @@ void _test_errors_new_cpp() {
error err = errors::New("hello world");
ASSERT_EQ(err->Error(), "hello world");
}
struct _MyError final : _error, object {
string msg;
string Error() {
return msg;
}
_MyError(const string& msg) : msg(msg) {}
~_MyError() {}
void incref() { object::incref(); }
void decref() {
if (object::__decref())
delete this;
}
};
struct _MyWrapError final : _errorWrapper, object {
string subj;
error err;
string Error() {
return subj + ": " + err->Error();
}
error Unwrap() {
return err;
}
_MyWrapError(string subj, error err) : subj(subj), err(err) {}
~_MyWrapError() {}
void incref() { object::incref(); }
void decref() {
if (object::__decref())
delete this;
}
};
typedef refptr<_MyError> MyError;
void _test_errors_unwrap_cpp() {
error err1, err2;
ASSERT_EQ(errors::Unwrap(/*nil*/error()), /*nil*/error());
err1 = adoptref(static_cast<_error*>(new _MyError("zzz")));
ASSERT_EQ(errors::Unwrap(err1), /*nil*/error());
_MyWrapError* _err2 = new _MyWrapError("aaa", err1);
err2 = adoptref(static_cast<_error*>(_err2));
ASSERT_EQ(errors::Unwrap(err2), err1);
// test err2.Unwrap() returning nil
_err2->err = nil;
ASSERT_EQ(_err2->Unwrap(), /*nil*/error());
ASSERT_EQ(errors::Unwrap(err2), /*nil*/error());
}
void _test_errors_is_cpp() {
auto E = errors::New;
ASSERT_EQ(errors::Is(/*nil*/error(), /*nil*/error()), true);
ASSERT_EQ(errors::Is(E("a"), /*nil*/error()), false);
ASSERT_EQ(errors::Is(/*nil*/error(), E("b")), false);
auto W = [](string subj, error err) -> error {
return adoptref(static_cast<_error*>(new _MyWrapError(subj, err)));
};
error ewrap = W("hello", W("world", E("мир")));
ASSERT_EQ(errors::Is(ewrap, E("мир")), true);
ASSERT_EQ(errors::Is(ewrap, E("май")), false);
ASSERT_EQ(errors::Is(ewrap, W("world", E("мир"))), true);
ASSERT_EQ(errors::Is(ewrap, W("hello", E("мир"))), false);
ASSERT_EQ(errors::Is(ewrap, W("hello", E("май"))), false);
ASSERT_EQ(errors::Is(ewrap, W("world", E("май"))), false);
ASSERT_EQ(errors::Is(ewrap, W("hello", W("world", E("мир")))), true);
ASSERT_EQ(errors::Is(ewrap, W("a", W("world", E("мир")))), false);
ASSERT_EQ(errors::Is(ewrap, W("hello", W("b", E("мир")))), false);
ASSERT_EQ(errors::Is(ewrap, W("hello", W("world", E("c")))), false);
ASSERT_EQ(errors::Is(ewrap, W("x", W("hello", W("world", E("мир"))))), false);
}
......@@ -24,9 +24,11 @@
#include "golang/fmt.h"
#include "golang/errors.h"
#include "golang/strings.h"
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
// golang::fmt::
......@@ -62,7 +64,7 @@ string sprintf(const char *format, ...) {
return str;
}
error errorf(const string &format, ...) {
error ___errorf(const string &format, ...) {
va_list argp;
va_start(argp, format);
error err = errors::New(fmt::_vsprintf(format.c_str(), argp));
......@@ -70,7 +72,7 @@ error errorf(const string &format, ...) {
return err;
}
error errorf(const char *format, ...) {
error ___errorf(const char *format, ...) {
va_list argp;
va_start(argp, format);
error err = errors::New(fmt::_vsprintf(format, argp));
......@@ -78,4 +80,90 @@ error errorf(const char *format, ...) {
return err;
}
// _WrapError is the error created by errorf("...: %w", err).
struct _WrapError final : _errorWrapper, object {
string _prefix;
error _errSuffix;
_WrapError(const string& prefix, error err) : _prefix(prefix), _errSuffix(err) {}
error Unwrap() { return _errSuffix; }
string Error() {
return _prefix + ": " +
(_errSuffix != nil ? _errSuffix->Error() : "%!w(<nil>)");
}
void incref() {
object::incref();
}
void decref() {
if (__decref())
delete this;
}
~_WrapError() {}
};
// ___errorfTryWrap serves __errorf(format, last_err, ...headv)
//
// NOTE it is called with ... = original argv with last err converted to
// err->Error().c_str() so that `errorf("... %s", ..., err)` also works.
error ___errorfTryWrap(const string& format, error last_err, ...) {
error err;
va_list argp;
va_start(argp, last_err);
if (strings::has_suffix(format, ": %w")) {
err = adoptref(static_cast<_error*>(
new _WrapError(
fmt::_vsprintf(strings::trim_suffix(format, ": %w").c_str(), argp),
last_err)));
}
else {
err = errors::New(fmt::_vsprintf(format.c_str(), argp));
}
va_end(argp);
return err;
}
error ___errorfTryWrap(const char *format, error last_err, ...) {
error err;
va_list argp;
va_start(argp, last_err);
const char *colon = strrchr(format, ':');
if (colon != NULL && strcmp(colon, ": %w") == 0) {
err = adoptref(static_cast<_error*>(
new _WrapError(
// TODO try to avoid std::string
fmt::_vsprintf(strings::trim_suffix(format, ": %w").c_str(), argp),
last_err)));
}
else {
err = errors::New(fmt::_vsprintf(format, argp));
}
va_end(argp);
return err;
}
// ___error_str is used by errorf to convert last_err into string for not %w.
//
// if we do not take nil into account the code crashes on nil->Error() call,
// which is unreasonable, because both Go and C printf family print something
// for nil instead of crash.
//
// string - not `const char*` - is returned, because returning
// err->Error().c_str() would be not correct as error string might be destructed
// at function exit, while if we return string, that would be correct and the
// caller can use returned data without crashing.
//
// we don't return %!s(<nil>) since we don't know whether %s was used in format
// tail or not.
string ___error_str(error err) {
return (err != nil ? err->Error() : "(<nil>)");
}
}} // golang::fmt::
#ifndef _NXD_LIBGOLANG_FMT_H
#define _NXD_LIBGOLANG_FMT_H
// Copyright (C) 2019 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
// Copyright (C) 2019-2020 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
......@@ -25,11 +25,15 @@
// - `sprintf` formats text into string.
// - `errorf` formats text into error.
//
// NOTE: formatting rules are those of libc, not Go.
// NOTE: with exception of %w, formatting rules are those of libc, not Go(*).
//
// See also https://golang.org/pkg/fmt for Go fmt package documentation.
//
// (*) errorf additionally handles Go-like %w to wrap an error similarly to
// https://blog.golang.org/go1.13-errors .
#include <golang/libgolang.h>
#include <type_traits>
// golang::fmt::
namespace golang {
......@@ -38,15 +42,108 @@ namespace fmt {
// sprintf formats text into string.
LIBGOLANG_API string sprintf(const string &format, ...);
// intseq<i1, i2, ...> and intrange<n> are used by errorf to handle %w.
namespace {
// intseq<i1, i2, ...> provides compile-time integer sequence.
// (std::integer_sequence is C++14 while libgolang targets C++11)
template<int ...nv>
struct intseq {
// apppend<x> defines intseq<i1, i2, ..., x>.
template<int x>
struct append {
using type = intseq<nv..., x>;
};
};
// intrange<n> provides integer sequence intseq<0, 1, 2, ..., n-1>.
template<int n>
struct intrange;
template<>
struct intrange<0> {
using type = intseq<>;
};
template<int n>
struct intrange {
using type = typename intrange<n-1>::type::template append<n-1>::type;
};
}
// `errorf` formats text into error.
LIBGOLANG_API error errorf (const string &format, ...);
//
// format suffix ": %w" is additionally handled as in Go with
// `errorf("... : %w", ..., err)` creating error that can be unwrapped back to err.
LIBGOLANG_API error ___errorf(const string& format, ...);
LIBGOLANG_API error ___errorfTryWrap(const string& format, error last_err, ...);
LIBGOLANG_API string ___error_str(error err);
// _errorf(..., err) tails here.
template<typename ...Headv>
inline error __errorf(std::true_type, const string& format, error last_err, Headv... headv) {
return ___errorfTryWrap(format, last_err, headv..., ___error_str(last_err).c_str());
}
// _errorf(..., !err) tails here.
template<typename ...Headv, typename Last>
inline error __errorf(std::false_type, const string& format, Last last, Headv... headv) {
return ___errorf(format, headv..., last);
}
template<typename ...Argv, int ...HeadIdxv>
inline error _errorf(intseq<HeadIdxv...>, const string& format, Argv... argv) {
auto argt = std::make_tuple(argv...);
auto last = std::get<sizeof...(argv)-1>(argt);
return __errorf(std::is_same<decltype(last), error>(), format, last, std::get<HeadIdxv>(argt)...);
}
inline error errorf(const string& format) {
return ___errorf(format);
}
template<typename ...Argv>
inline error errorf(const string& format, Argv... argv) {
return _errorf(typename intrange<sizeof...(argv)-1>::type(), format, argv...);
}
// `const char *` overloads just to catch format mistakes as
// __attribute__(format) does not work with std::string.
LIBGOLANG_API string sprintf(const char *format, ...)
__attribute__ ((format (printf, 1, 2)));
LIBGOLANG_API error errorf (const char *format, ...)
__attribute__ ((format (printf, 1, 2)));
// cannot use __attribute__(format) for errorf as we add %w handling.
// still `const char *` overload is useful for performance.
LIBGOLANG_API error ___errorf(const char *format, ...);
LIBGOLANG_API error ___errorfTryWrap(const char *format, error last_err, ...);
// _errorf(..., err) tails here.
template<typename ...Headv>
inline error __errorf(std::true_type, const char *format, error last_err, Headv... headv) {
return ___errorfTryWrap(format, last_err, headv..., ___error_str(last_err).c_str());
}
// _errorf(..., !err) tails here.
template<typename ...Headv, typename Last>
inline error __errorf(std::false_type, const char *format, Last last, Headv... headv) {
return ___errorf(format, headv..., last);
}
template<typename ...Argv, int ...HeadIdxv>
inline error _errorf(intseq<HeadIdxv...>, const char *format, Argv... argv) {
auto argt = std::make_tuple(argv...);
auto last = std::get<sizeof...(argv)-1>(argt);
return __errorf(std::is_same<decltype(last), error>(), format, last, std::get<HeadIdxv>(argt)...);
}
inline error errorf(const char *format) {
return ___errorf(format);
}
template<typename ...Argv>
inline error errorf(const char *format, Argv... argv) {
return _errorf(typename intrange<sizeof...(argv)-1>::type(), format, argv...);
}
}} // golang::fmt::
......
......@@ -18,31 +18,80 @@
// See https://www.nexedi.com/licensing for rationale and options.
#include "golang/fmt.h"
#include "golang/errors.h"
#include "golang/_testing.h"
using namespace golang;
void _test_fmt_sprintf_cpp() {
// NOTE not using vargs helper, since sprintf itself uses vargs and we want
// to test varg logic there for correctness too.
ASSERT_EQ(fmt::sprintf("") , "");
ASSERT_EQ(fmt::sprintf("hello world") , "hello world");
ASSERT_EQ(fmt::sprintf("hello %d zzz", 123) , "hello 123 zzz");
ASSERT_EQ(fmt::sprintf("%s %s: %s", "read", "myfile", "myerror") , "read myfile: myerror");
// with string format (not `const char *`)
ASSERT_EQ(fmt::sprintf(string("")) , "");
const char *myfile = "myfile";
const char *myerror = "myerror";
string f = "%s %s: %s";
const char *myfile = "myfile";
ASSERT_EQ(fmt::sprintf(f, "read", myfile, myerror) , "read myfile: myerror");
ASSERT_EQ(fmt::sprintf(string("%s %s: %s"), "read", myfile, myerror) , "read myfile: myerror");
}
void _test_fmt_errorf_cpp() {
error myerr = errors::New("myerror");
error myerrWrap, myerr2;
ASSERT_EQ(fmt::errorf("")->Error() , "");
ASSERT_EQ(fmt::errorf("hello world")->Error() , "hello world");
ASSERT_EQ(fmt::errorf("hello %d zzz", 123)->Error() , "hello 123 zzz");
ASSERT_EQ(fmt::errorf("%s %s: %s", "read", "myfile", "myerror")->Error() , "read myfile: myerror");
myerrWrap = fmt::errorf("%s %s: %w", "read", "myfile", myerr);
ASSERT_EQ(myerrWrap->Error(), "read myfile: myerror");
myerr2 = errors::Unwrap(myerrWrap);
ASSERT_EQ(myerr2, myerr);
myerrWrap = fmt::errorf("%s %s: %s", "read", "myfile", myerr);
ASSERT_EQ(myerrWrap->Error(), "read myfile: myerror");
myerr2 = errors::Unwrap(myerrWrap);
ASSERT_EQ(myerr2, /*nil*/error());
// with string format (not `const char *`)
const char *myerror = "myerror";
string f = "%s %s: %s";
ASSERT_EQ(fmt::errorf(string(""))->Error() , "");
const char *esmth = "esmth";
const char *myfile = "myfile";
ASSERT_EQ(fmt::errorf(f, "read", myfile, myerror)->Error() , "read myfile: myerror");
ASSERT_EQ(fmt::errorf(string("%s %s: %s"), "read", myfile, esmth)->Error() , "read myfile: esmth");
myerrWrap = fmt::errorf(string("%s %s: %w"), "read", myfile, myerr);
ASSERT_EQ(myerrWrap->Error(), "read myfile: myerror");
myerr2 = errors::Unwrap(myerrWrap);
ASSERT_EQ(myerr2, myerr);
myerrWrap = fmt::errorf(string("%s %s: %s"), "read", myfile, myerr);
ASSERT_EQ(myerrWrap->Error(), "read myfile: myerror");
myerr2 = errors::Unwrap(myerrWrap);
ASSERT_EQ(myerr2, /*nil*/error());
// %w with nil
myerr = nil;
myerrWrap = fmt::errorf("%s %s: %w", "read", "myfile", myerr);
ASSERT_EQ(myerrWrap->Error(), "read myfile: %!w(<nil>)");
myerr2 = errors::Unwrap(myerrWrap);
ASSERT_EQ(myerr2, /*nil*/error());
myerrWrap = fmt::errorf(string("%s %s: %w"), "read", "myfile", myerr);
ASSERT_EQ(myerrWrap->Error(), "read myfile: %!w(<nil>)");
myerr2 = errors::Unwrap(myerrWrap);
ASSERT_EQ(myerr2, /*nil*/error());
// %s with nil
myerrWrap = fmt::errorf("%s %s: %s", "read", "myfile", myerr);
ASSERT_EQ(myerrWrap->Error(), "read myfile: (<nil>)");
myerr2 = errors::Unwrap(myerrWrap);
ASSERT_EQ(myerr2, /*nil*/error());
myerrWrap = fmt::errorf(string("%s %s: %s"), "read", "myfile", myerr);
ASSERT_EQ(myerrWrap->Error(), "read myfile: (<nil>)");
myerr2 = errors::Unwrap(myerrWrap);
ASSERT_EQ(myerr2, /*nil*/error());
}
......@@ -798,6 +798,12 @@ struct _error : _interface {
};
typedef refptr<_error> error;
// an error can additionally provide Unwrap method if it wraps another error.
struct _errorWrapper : _error {
virtual error Unwrap() = 0;
};
typedef refptr<_errorWrapper> errorWrapper;
} // golang::
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment