refactor(netxlite/errors): improve docs and format code (#481)

No real functional change. A few are needed and they will come
next. With this diff I just wanted to do cosmetic changes and
documentation changes, to ensure this package is okay.

See https://github.com/ooni/probe/issues/1591
This commit is contained in:
Simone Basso 2021-09-07 20:39:32 +02:00 committed by GitHub
parent 323266da83
commit a56b284b0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 346 additions and 135 deletions

View File

@ -18,6 +18,26 @@ import (
// ClassifyGenericError is the generic classifier mapping an error
// occurred during an operation to an OONI failure string.
//
// If the input error is already an ErrWrapper we don't perform
// the classification again and we return its Failure to the caller.
//
// Classification rules
//
// We put inside this classifier:
//
// - system call errors
//
// - generic errors that can occur in multiple places
//
// - all the errors that depend on strings
//
// The more specific classifiers will call this classifier if
// they fail to find a mapping for the input error.
//
// If everything else fails, this classifier returns a string
// like "unknown_failure: XXX" where XXX has been scrubbed
// so to remove any network endpoints from its value.
func ClassifyGenericError(err error) string {
// The list returned here matches the values used by MK unless
// explicitly noted otherwise with a comment.
@ -27,6 +47,9 @@ func ClassifyGenericError(err error) string {
return errwrapper.Error() // we've already wrapped it
}
// Classify system errors first. We could use strings for many
// of them on Unix, but this would fail on Windows as described
// by https://github.com/ooni/probe/issues/1526.
if failure := classifySyscallError(err); failure != "" {
return failure
}
@ -34,6 +57,7 @@ func ClassifyGenericError(err error) string {
if errors.Is(err, context.Canceled) {
return FailureInterrupted
}
s := err.Error()
if strings.HasSuffix(s, "operation was canceled") {
return FailureInterrupted
@ -50,7 +74,6 @@ func ClassifyGenericError(err error) string {
if strings.HasSuffix(s, "i/o timeout") {
return FailureGenericTimeoutError
}
// TODO(kelmenhorst,bassosimone): this can probably be moved since it's TLS specific
if strings.HasSuffix(s, "TLS handshake timeout") {
return FailureGenericTimeoutError
}
@ -60,11 +83,13 @@ func ClassifyGenericError(err error) string {
// that we return here is significantly more specific.
return FailureDNSNXDOMAINError
}
formatted := fmt.Sprintf("unknown_failure: %s", s)
return scrubber.Scrub(formatted) // scrub IP addresses in the error
}
// TLS alert protocol as defined in RFC8446
// TLS alert protocol as defined in RFC8446. We need these definitions
// to figure out which error occurred during a QUIC handshake.
const (
// Sender was unable to negotiate an acceptable set of security parameters given the options available.
quicTLSAlertHandshakeFailure = 40
@ -94,6 +119,11 @@ const (
quicTLSUnrecognizedName = 112
)
// quicIsCertificateError tells us whether a specific TLS alert error
// we received is actually an error depending on the certificate.
//
// The set of checks we implement here is a set of heuristics based
// on our understanding of the TLS spec and may need tweaks.
func quicIsCertificateError(alert uint8) bool {
return (alert == quicTLSAlertBadCertificate ||
alert == quicTLSAlertUnsupportedCertificate ||
@ -104,17 +134,25 @@ func quicIsCertificateError(alert uint8) bool {
// ClassifyQUICHandshakeError maps an error occurred during the QUIC
// handshake to an OONI failure string.
//
// If the input error is already an ErrWrapper we don't perform
// the classification again and we return its Failure to the caller.
//
// If this classifier fails, it calls ClassifyGenericError and
// returns to the caller its return value.
func ClassifyQUICHandshakeError(err error) string {
var errwrapper *ErrWrapper
if errors.As(err, &errwrapper) {
return errwrapper.Error() // we've already wrapped it
}
var versionNegotiation *quic.VersionNegotiationError
var statelessReset *quic.StatelessResetError
var handshakeTimeout *quic.HandshakeTimeoutError
var idleTimeout *quic.IdleTimeoutError
var transportError *quic.TransportError
var (
versionNegotiation *quic.VersionNegotiationError
statelessReset *quic.StatelessResetError
handshakeTimeout *quic.HandshakeTimeoutError
idleTimeout *quic.IdleTimeoutError
transportError *quic.TransportError
)
if errors.As(err, &versionNegotiation) {
return FailureQUICIncompatibleVersion
@ -137,8 +175,9 @@ func ClassifyQUICHandshakeError(err error) string {
if quicIsCertificateError(errCode) {
return FailureSSLInvalidCertificate
}
// TLSAlertDecryptError and TLSAlertHandshakeFailure are summarized to a FailureSSLHandshake error because both
// alerts are caused by a failed or corrupted parameter negotiation during the TLS handshake.
// TLSAlertDecryptError and TLSAlertHandshakeFailure are summarized to a
// FailureSSLHandshake error because both alerts are caused by a failed or
// corrupted parameter negotiation during the TLS handshake.
if errCode == quicTLSAlertDecryptError || errCode == quicTLSAlertHandshakeFailure {
return FailureSSLFailedHandshake
}
@ -152,13 +191,18 @@ func ClassifyQUICHandshakeError(err error) string {
return ClassifyGenericError(err)
}
// ErrDNSBogon indicates that we found a bogon address. This is the
// correct value with which to initialize MeasurementRoot.ErrDNSBogon
// to tell this library to return an error when a bogon is found.
// ErrDNSBogon indicates that we found a bogon address. Code that
// filters for DNS bogons MUST use this error.
var ErrDNSBogon = errors.New("dns: detected bogon address")
// ClassifyResolverError maps an error occurred during a domain name
// resolution to the corresponding OONI failure string.
//
// If the input error is already an ErrWrapper we don't perform
// the classification again and we return its Failure to the caller.
//
// If this classifier fails, it calls ClassifyGenericError and
// returns to the caller its return value.
func ClassifyResolverError(err error) string {
var errwrapper *ErrWrapper
if errors.As(err, &errwrapper) {
@ -172,6 +216,12 @@ func ClassifyResolverError(err error) string {
// ClassifyTLSHandshakeError maps an error occurred during the TLS
// handshake to an OONI failure string.
//
// If the input error is already an ErrWrapper we don't perform
// the classification again and we return its Failure to the caller.
//
// If this classifier fails, it calls ClassifyGenericError and
// returns to the caller its return value.
func ClassifyTLSHandshakeError(err error) string {
var errwrapper *ErrWrapper
if errors.As(err, &errwrapper) {

View File

@ -22,52 +22,62 @@ func TestClassifyGenericError(t *testing.T) {
t.Fatal("did not classify existing ErrWrapper correctly")
}
})
t.Run("for already wrapped error", func(t *testing.T) {
err := io.EOF
if ClassifyGenericError(err) != FailureEOFError {
t.Fatal("unexpected result")
}
})
t.Run("for context.Canceled", func(t *testing.T) {
if ClassifyGenericError(context.Canceled) != FailureInterrupted {
t.Fatal("unexpected result")
}
})
t.Run("for operation was canceled error", func(t *testing.T) {
if ClassifyGenericError(errors.New("operation was canceled")) != FailureInterrupted {
t.Fatal("unexpected result")
}
})
t.Run("for EOF", func(t *testing.T) {
if ClassifyGenericError(io.EOF) != FailureEOFError {
t.Fatal("unexpected results")
}
})
t.Run("for canceled", func(t *testing.T) {
if ClassifyGenericError(syscall.ECANCELED) != FailureOperationCanceled {
t.Fatal("unexpected results")
}
})
t.Run("for connection_refused", func(t *testing.T) {
if ClassifyGenericError(syscall.ECONNREFUSED) != FailureConnectionRefused {
t.Fatal("unexpected results")
}
})
t.Run("for connection_reset", func(t *testing.T) {
if ClassifyGenericError(syscall.ECONNRESET) != FailureConnectionReset {
t.Fatal("unexpected results")
}
})
t.Run("for host_unreachable", func(t *testing.T) {
if ClassifyGenericError(syscall.EHOSTUNREACH) != FailureHostUnreachable {
t.Fatal("unexpected results")
}
})
t.Run("for system timeout", func(t *testing.T) {
if ClassifyGenericError(syscall.ETIMEDOUT) != FailureTimedOut {
t.Fatal("unexpected results")
}
})
t.Run("for context deadline exceeded", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1)
defer cancel()
@ -76,11 +86,13 @@ func TestClassifyGenericError(t *testing.T) {
t.Fatal("unexpected results")
}
})
t.Run("for stun's transaction is timed out", func(t *testing.T) {
if ClassifyGenericError(stun.ErrTransactionTimeOut) != FailureGenericTimeoutError {
t.Fatal("unexpected results")
}
})
t.Run("for i/o error", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1)
defer cancel() // fail immediately
@ -95,12 +107,14 @@ func TestClassifyGenericError(t *testing.T) {
t.Fatal("unexpected results")
}
})
t.Run("for TLS handshake timeout error", func(t *testing.T) {
err := errors.New("net/http: TLS handshake timeout")
if ClassifyGenericError(err) != FailureGenericTimeoutError {
t.Fatal("unexpected results")
}
})
t.Run("for no such host", func(t *testing.T) {
if ClassifyGenericError(&net.DNSError{
Err: "no such host",
@ -108,6 +122,7 @@ func TestClassifyGenericError(t *testing.T) {
t.Fatal("unexpected results")
}
})
t.Run("for errors including IPv4 address", func(t *testing.T) {
input := errors.New("read tcp 10.0.2.15:56948->93.184.216.34:443: use of closed network connection")
expected := "unknown_failure: read tcp [scrubbed]->[scrubbed]: use of closed network connection"
@ -116,6 +131,7 @@ func TestClassifyGenericError(t *testing.T) {
t.Fatal(cmp.Diff(expected, out))
}
})
t.Run("for errors including IPv6 address", func(t *testing.T) {
input := errors.New("read tcp [::1]:56948->[::1]:443: use of closed network connection")
expected := "unknown_failure: read tcp [scrubbed]->[scrubbed]: use of closed network connection"
@ -124,6 +140,7 @@ func TestClassifyGenericError(t *testing.T) {
t.Fatal(cmp.Diff(expected, out))
}
})
t.Run("for i/o error", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1)
defer cancel() // fail immediately
@ -152,55 +169,65 @@ func TestClassifyQUICHandshakeError(t *testing.T) {
t.Fatal("did not classify existing ErrWrapper correctly")
}
})
t.Run("for connection_reset", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.StatelessResetError{}) != FailureConnectionReset {
t.Fatal("unexpected results")
}
})
t.Run("for incompatible quic version", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.VersionNegotiationError{}) != FailureQUICIncompatibleVersion {
t.Fatal("unexpected results")
}
})
t.Run("for quic connection refused", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: quic.ConnectionRefused}) != FailureConnectionRefused {
t.Fatal("unexpected results")
}
})
t.Run("for quic handshake timeout", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.HandshakeTimeoutError{}) != FailureGenericTimeoutError {
t.Fatal("unexpected results")
}
})
t.Run("for QUIC idle connection timeout", func(t *testing.T) {
if ClassifyQUICHandshakeError(&quic.IdleTimeoutError{}) != FailureGenericTimeoutError {
t.Fatal("unexpected results")
}
})
t.Run("for QUIC CRYPTO Handshake", func(t *testing.T) {
var err quic.TransportErrorCode = quicTLSAlertHandshakeFailure
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLFailedHandshake {
t.Fatal("unexpected results")
}
})
t.Run("for QUIC CRYPTO Invalid Certificate", func(t *testing.T) {
var err quic.TransportErrorCode = quicTLSAlertBadCertificate
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidCertificate {
t.Fatal("unexpected results")
}
})
t.Run("for QUIC CRYPTO Unknown CA", func(t *testing.T) {
var err quic.TransportErrorCode = quicTLSAlertUnknownCA
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLUnknownAuthority {
t.Fatal("unexpected results")
}
})
t.Run("for QUIC CRYPTO Bad Hostname", func(t *testing.T) {
var err quic.TransportErrorCode = quicTLSUnrecognizedName
if ClassifyQUICHandshakeError(&quic.TransportError{ErrorCode: err}) != FailureSSLInvalidHostname {
t.Fatal("unexpected results")
}
})
t.Run("for another kind of error", func(t *testing.T) {
if ClassifyQUICHandshakeError(io.EOF) != FailureEOFError {
t.Fatal("unexpected result")
@ -215,11 +242,13 @@ func TestClassifyResolverError(t *testing.T) {
t.Fatal("did not classify existing ErrWrapper correctly")
}
})
t.Run("for ErrDNSBogon", func(t *testing.T) {
if ClassifyResolverError(ErrDNSBogon) != FailureDNSBogonError {
t.Fatal("unexpected result")
}
})
t.Run("for another kind of error", func(t *testing.T) {
if ClassifyResolverError(io.EOF) != FailureEOFError {
t.Fatal("unexpected result")
@ -234,24 +263,28 @@ func TestClassifyTLSHandshakeError(t *testing.T) {
t.Fatal("did not classify existing ErrWrapper correctly")
}
})
t.Run("for x509.HostnameError", func(t *testing.T) {
var err x509.HostnameError
if ClassifyTLSHandshakeError(err) != FailureSSLInvalidHostname {
t.Fatal("unexpected result")
}
})
t.Run("for x509.UnknownAuthorityError", func(t *testing.T) {
var err x509.UnknownAuthorityError
if ClassifyTLSHandshakeError(err) != FailureSSLUnknownAuthority {
t.Fatal("unexpected result")
}
})
t.Run("for x509.CertificateInvalidError", func(t *testing.T) {
var err x509.CertificateInvalidError
if ClassifyTLSHandshakeError(err) != FailureSSLInvalidCertificate {
t.Fatal("unexpected result")
}
})
t.Run("for another kind of error", func(t *testing.T) {
if ClassifyTLSHandshakeError(io.EOF) != FailureEOFError {
t.Fatal("unexpected result")

View File

@ -1,2 +1,20 @@
// Package errorsx contains code to classify errors.
//
// We define the ErrWrapper type, that should wrap any error
// and map it to the corresponding OONI failure.
//
// See https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md
// for a list of OONI failure strings.
//
// We define ClassifyXXX functions that map an `error` type to
// the corresponding OONI failure.
//
// When we cannot map an error to an OONI failure we return
// an "unknown_failure: XXX" string where the XXX part has
// been scrubbed so to remove any network endpoints.
//
// The general approach we have been following for this
// package has been to return the same strings that we used
// with the previous measurement engine, Measurement Kit
// available at https://github.com/measurement-kit/measurement-kit.
package errorsx

View File

@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
// Generated: 2021-09-07 16:43:08.462721 +0200 CEST m=+0.105415376
// Generated: 2021-09-07 20:26:06.502417 +0200 CEST m=+0.133209876
package errorsx

View File

@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
// Generated: 2021-09-07 16:43:08.510432 +0200 CEST m=+0.153127376
// Generated: 2021-09-07 20:26:06.547786 +0200 CEST m=+0.178579584
package errorsx
@ -9,95 +9,184 @@ import (
"testing"
)
func TestToSyscallErr(t *testing.T) {
if v := classifySyscallError(io.EOF); v != "" {
t.Fatalf("expected empty string, got '%s'", v)
}
if v := classifySyscallError(ECANCELED); v != FailureOperationCanceled {
t.Fatalf("expected '%s', got '%s'", FailureOperationCanceled, v)
}
if v := classifySyscallError(ECONNREFUSED); v != FailureConnectionRefused {
t.Fatalf("expected '%s', got '%s'", FailureConnectionRefused, v)
}
if v := classifySyscallError(ECONNRESET); v != FailureConnectionReset {
t.Fatalf("expected '%s', got '%s'", FailureConnectionReset, v)
}
if v := classifySyscallError(EHOSTUNREACH); v != FailureHostUnreachable {
t.Fatalf("expected '%s', got '%s'", FailureHostUnreachable, v)
}
if v := classifySyscallError(ETIMEDOUT); v != FailureTimedOut {
t.Fatalf("expected '%s', got '%s'", FailureTimedOut, v)
}
if v := classifySyscallError(EAFNOSUPPORT); v != FailureAddressFamilyNotSupported {
t.Fatalf("expected '%s', got '%s'", FailureAddressFamilyNotSupported, v)
}
if v := classifySyscallError(EADDRINUSE); v != FailureAddressInUse {
t.Fatalf("expected '%s', got '%s'", FailureAddressInUse, v)
}
if v := classifySyscallError(EADDRNOTAVAIL); v != FailureAddressNotAvailable {
t.Fatalf("expected '%s', got '%s'", FailureAddressNotAvailable, v)
}
if v := classifySyscallError(EISCONN); v != FailureAlreadyConnected {
t.Fatalf("expected '%s', got '%s'", FailureAlreadyConnected, v)
}
if v := classifySyscallError(EFAULT); v != FailureBadAddress {
t.Fatalf("expected '%s', got '%s'", FailureBadAddress, v)
}
if v := classifySyscallError(EBADF); v != FailureBadFileDescriptor {
t.Fatalf("expected '%s', got '%s'", FailureBadFileDescriptor, v)
}
if v := classifySyscallError(ECONNABORTED); v != FailureConnectionAborted {
t.Fatalf("expected '%s', got '%s'", FailureConnectionAborted, v)
}
if v := classifySyscallError(EALREADY); v != FailureConnectionAlreadyInProgress {
t.Fatalf("expected '%s', got '%s'", FailureConnectionAlreadyInProgress, v)
}
if v := classifySyscallError(EDESTADDRREQ); v != FailureDestinationAddressRequired {
t.Fatalf("expected '%s', got '%s'", FailureDestinationAddressRequired, v)
}
if v := classifySyscallError(EINTR); v != FailureInterrupted {
t.Fatalf("expected '%s', got '%s'", FailureInterrupted, v)
}
if v := classifySyscallError(EINVAL); v != FailureInvalidArgument {
t.Fatalf("expected '%s', got '%s'", FailureInvalidArgument, v)
}
if v := classifySyscallError(EMSGSIZE); v != FailureMessageSize {
t.Fatalf("expected '%s', got '%s'", FailureMessageSize, v)
}
if v := classifySyscallError(ENETDOWN); v != FailureNetworkDown {
t.Fatalf("expected '%s', got '%s'", FailureNetworkDown, v)
}
if v := classifySyscallError(ENETRESET); v != FailureNetworkReset {
t.Fatalf("expected '%s', got '%s'", FailureNetworkReset, v)
}
if v := classifySyscallError(ENETUNREACH); v != FailureNetworkUnreachable {
t.Fatalf("expected '%s', got '%s'", FailureNetworkUnreachable, v)
}
if v := classifySyscallError(ENOBUFS); v != FailureNoBufferSpace {
t.Fatalf("expected '%s', got '%s'", FailureNoBufferSpace, v)
}
if v := classifySyscallError(ENOPROTOOPT); v != FailureNoProtocolOption {
t.Fatalf("expected '%s', got '%s'", FailureNoProtocolOption, v)
}
if v := classifySyscallError(ENOTSOCK); v != FailureNotASocket {
t.Fatalf("expected '%s', got '%s'", FailureNotASocket, v)
}
if v := classifySyscallError(ENOTCONN); v != FailureNotConnected {
t.Fatalf("expected '%s', got '%s'", FailureNotConnected, v)
}
if v := classifySyscallError(EWOULDBLOCK); v != FailureOperationWouldBlock {
t.Fatalf("expected '%s', got '%s'", FailureOperationWouldBlock, v)
}
if v := classifySyscallError(EACCES); v != FailurePermissionDenied {
t.Fatalf("expected '%s', got '%s'", FailurePermissionDenied, v)
}
if v := classifySyscallError(EPROTONOSUPPORT); v != FailureProtocolNotSupported {
t.Fatalf("expected '%s', got '%s'", FailureProtocolNotSupported, v)
}
if v := classifySyscallError(EPROTOTYPE); v != FailureWrongProtocolType {
t.Fatalf("expected '%s', got '%s'", FailureWrongProtocolType, v)
}
if v := classifySyscallError(syscall.Errno(0)); v != "" {
t.Fatalf("expected empty string, got '%s'", v)
}
func TestClassifySyscallError(t *testing.T) {
t.Run("for a non-syscall error", func(t *testing.T) {
if v := classifySyscallError(io.EOF); v != "" {
t.Fatalf("expected empty string, got '%s'", v)
}
})
t.Run("for ECANCELED", func(t *testing.T) {
if v := classifySyscallError(ECANCELED); v != FailureOperationCanceled {
t.Fatalf("expected '%s', got '%s'", FailureOperationCanceled, v)
}
})
t.Run("for ECONNREFUSED", func(t *testing.T) {
if v := classifySyscallError(ECONNREFUSED); v != FailureConnectionRefused {
t.Fatalf("expected '%s', got '%s'", FailureConnectionRefused, v)
}
})
t.Run("for ECONNRESET", func(t *testing.T) {
if v := classifySyscallError(ECONNRESET); v != FailureConnectionReset {
t.Fatalf("expected '%s', got '%s'", FailureConnectionReset, v)
}
})
t.Run("for EHOSTUNREACH", func(t *testing.T) {
if v := classifySyscallError(EHOSTUNREACH); v != FailureHostUnreachable {
t.Fatalf("expected '%s', got '%s'", FailureHostUnreachable, v)
}
})
t.Run("for ETIMEDOUT", func(t *testing.T) {
if v := classifySyscallError(ETIMEDOUT); v != FailureTimedOut {
t.Fatalf("expected '%s', got '%s'", FailureTimedOut, v)
}
})
t.Run("for EAFNOSUPPORT", func(t *testing.T) {
if v := classifySyscallError(EAFNOSUPPORT); v != FailureAddressFamilyNotSupported {
t.Fatalf("expected '%s', got '%s'", FailureAddressFamilyNotSupported, v)
}
})
t.Run("for EADDRINUSE", func(t *testing.T) {
if v := classifySyscallError(EADDRINUSE); v != FailureAddressInUse {
t.Fatalf("expected '%s', got '%s'", FailureAddressInUse, v)
}
})
t.Run("for EADDRNOTAVAIL", func(t *testing.T) {
if v := classifySyscallError(EADDRNOTAVAIL); v != FailureAddressNotAvailable {
t.Fatalf("expected '%s', got '%s'", FailureAddressNotAvailable, v)
}
})
t.Run("for EISCONN", func(t *testing.T) {
if v := classifySyscallError(EISCONN); v != FailureAlreadyConnected {
t.Fatalf("expected '%s', got '%s'", FailureAlreadyConnected, v)
}
})
t.Run("for EFAULT", func(t *testing.T) {
if v := classifySyscallError(EFAULT); v != FailureBadAddress {
t.Fatalf("expected '%s', got '%s'", FailureBadAddress, v)
}
})
t.Run("for EBADF", func(t *testing.T) {
if v := classifySyscallError(EBADF); v != FailureBadFileDescriptor {
t.Fatalf("expected '%s', got '%s'", FailureBadFileDescriptor, v)
}
})
t.Run("for ECONNABORTED", func(t *testing.T) {
if v := classifySyscallError(ECONNABORTED); v != FailureConnectionAborted {
t.Fatalf("expected '%s', got '%s'", FailureConnectionAborted, v)
}
})
t.Run("for EALREADY", func(t *testing.T) {
if v := classifySyscallError(EALREADY); v != FailureConnectionAlreadyInProgress {
t.Fatalf("expected '%s', got '%s'", FailureConnectionAlreadyInProgress, v)
}
})
t.Run("for EDESTADDRREQ", func(t *testing.T) {
if v := classifySyscallError(EDESTADDRREQ); v != FailureDestinationAddressRequired {
t.Fatalf("expected '%s', got '%s'", FailureDestinationAddressRequired, v)
}
})
t.Run("for EINTR", func(t *testing.T) {
if v := classifySyscallError(EINTR); v != FailureInterrupted {
t.Fatalf("expected '%s', got '%s'", FailureInterrupted, v)
}
})
t.Run("for EINVAL", func(t *testing.T) {
if v := classifySyscallError(EINVAL); v != FailureInvalidArgument {
t.Fatalf("expected '%s', got '%s'", FailureInvalidArgument, v)
}
})
t.Run("for EMSGSIZE", func(t *testing.T) {
if v := classifySyscallError(EMSGSIZE); v != FailureMessageSize {
t.Fatalf("expected '%s', got '%s'", FailureMessageSize, v)
}
})
t.Run("for ENETDOWN", func(t *testing.T) {
if v := classifySyscallError(ENETDOWN); v != FailureNetworkDown {
t.Fatalf("expected '%s', got '%s'", FailureNetworkDown, v)
}
})
t.Run("for ENETRESET", func(t *testing.T) {
if v := classifySyscallError(ENETRESET); v != FailureNetworkReset {
t.Fatalf("expected '%s', got '%s'", FailureNetworkReset, v)
}
})
t.Run("for ENETUNREACH", func(t *testing.T) {
if v := classifySyscallError(ENETUNREACH); v != FailureNetworkUnreachable {
t.Fatalf("expected '%s', got '%s'", FailureNetworkUnreachable, v)
}
})
t.Run("for ENOBUFS", func(t *testing.T) {
if v := classifySyscallError(ENOBUFS); v != FailureNoBufferSpace {
t.Fatalf("expected '%s', got '%s'", FailureNoBufferSpace, v)
}
})
t.Run("for ENOPROTOOPT", func(t *testing.T) {
if v := classifySyscallError(ENOPROTOOPT); v != FailureNoProtocolOption {
t.Fatalf("expected '%s', got '%s'", FailureNoProtocolOption, v)
}
})
t.Run("for ENOTSOCK", func(t *testing.T) {
if v := classifySyscallError(ENOTSOCK); v != FailureNotASocket {
t.Fatalf("expected '%s', got '%s'", FailureNotASocket, v)
}
})
t.Run("for ENOTCONN", func(t *testing.T) {
if v := classifySyscallError(ENOTCONN); v != FailureNotConnected {
t.Fatalf("expected '%s', got '%s'", FailureNotConnected, v)
}
})
t.Run("for EWOULDBLOCK", func(t *testing.T) {
if v := classifySyscallError(EWOULDBLOCK); v != FailureOperationWouldBlock {
t.Fatalf("expected '%s', got '%s'", FailureOperationWouldBlock, v)
}
})
t.Run("for EACCES", func(t *testing.T) {
if v := classifySyscallError(EACCES); v != FailurePermissionDenied {
t.Fatalf("expected '%s', got '%s'", FailurePermissionDenied, v)
}
})
t.Run("for EPROTONOSUPPORT", func(t *testing.T) {
if v := classifySyscallError(EPROTONOSUPPORT); v != FailureProtocolNotSupported {
t.Fatalf("expected '%s', got '%s'", FailureProtocolNotSupported, v)
}
})
t.Run("for EPROTOTYPE", func(t *testing.T) {
if v := classifySyscallError(EPROTOTYPE); v != FailureWrongProtocolType {
t.Fatalf("expected '%s', got '%s'", FailureWrongProtocolType, v)
}
})
t.Run("for the zero errno value", func(t *testing.T) {
if v := classifySyscallError(syscall.Errno(0)); v != "" {
t.Fatalf("expected empty string, got '%s'", v)
}
})
}

View File

@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
// Generated: 2021-09-07 16:43:08.35751 +0200 CEST m=+0.000202959
// Generated: 2021-09-07 20:26:06.370246 +0200 CEST m=+0.001036417
package errorsx

View File

@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
// Generated: 2021-09-07 16:43:08.436584 +0200 CEST m=+0.079277834
// Generated: 2021-09-07 20:26:06.478577 +0200 CEST m=+0.109369751
package errorsx

View File

@ -2,17 +2,28 @@ package errorsx
// ErrWrapper is our error wrapper for Go errors. The key objective of
// this structure is to properly set Failure, which is also returned by
// the Error() method, so be one of the OONI defined strings.
// the Error() method, to be one of the OONI failure strings.
//
// OONI failure strings are defined in the github.com/ooni/spec repo
// at https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md.
type ErrWrapper struct {
// Failure is the OONI failure string. The failure strings are
// loosely backward compatible with Measurement Kit.
//
// This is either one of the FailureXXX strings or any other
// string like `unknown_failure ...`. The latter represents an
// string like `unknown_failure: ...`. The latter represents an
// error that we have not yet mapped to a failure.
Failure string
// Operation is the operation that failed. If possible, it
// Operation is the operation that failed.
//
// New code will always nest ErrWrapper and you need to
// walk the chain to find what happened.
//
// The following comment describes the DEPRECATED
// legacy behavior implements by internal/engine/legacy/errorsx:
//
// If possible, the Operation string
// SHOULD be a _major_ operation. Major operations are:
//
// - ResolveOperation: resolving a domain name failed

View File

@ -6,19 +6,21 @@ import (
"testing"
)
func TestErrWrapperError(t *testing.T) {
err := &ErrWrapper{Failure: FailureDNSNXDOMAINError}
if err.Error() != FailureDNSNXDOMAINError {
t.Fatal("invalid return value")
}
}
func TestErrWrapper(t *testing.T) {
t.Run("Error", func(t *testing.T) {
err := &ErrWrapper{Failure: FailureDNSNXDOMAINError}
if err.Error() != FailureDNSNXDOMAINError {
t.Fatal("invalid return value")
}
})
func TestErrWrapperUnwrap(t *testing.T) {
err := &ErrWrapper{
Failure: FailureEOFError,
WrappedErr: io.EOF,
}
if !errors.Is(err, io.EOF) {
t.Fatal("cannot unwrap error")
}
t.Run("Unwrap", func(t *testing.T) {
err := &ErrWrapper{
Failure: FailureEOFError,
WrappedErr: io.EOF,
}
if !errors.Is(err, io.EOF) {
t.Fatal("cannot unwrap error")
}
})
}

View File

@ -234,25 +234,32 @@ func writeGenericTestFile() {
fileWrite(filep, "\t\"testing\"\n")
fileWrite(filep, ")\n\n")
fileWrite(filep, "func TestToSyscallErr(t *testing.T) {\n")
fileWrite(filep, "\tif v := classifySyscallError(io.EOF); v != \"\" {\n")
fileWrite(filep, "\t\tt.Fatalf(\"expected empty string, got '%s'\", v)\n")
fileWrite(filep, "\t}\n")
fileWrite(filep, "func TestClassifySyscallError(t *testing.T) {\n")
fileWrite(filep, "\tt.Run(\"for a non-syscall error\", func (t *testing.T) {\n")
fileWrite(filep, "\t\tif v := classifySyscallError(io.EOF); v != \"\" {\n")
fileWrite(filep, "\t\t\tt.Fatalf(\"expected empty string, got '%s'\", v)\n")
fileWrite(filep, "\t\t}\n")
fileWrite(filep, "\t})\n\n")
for _, spec := range Specs {
if !spec.IsSystemError() {
continue
}
filePrintf(filep, "\tif v := classifySyscallError(%s); v != %s {\n",
filePrintf(filep, "\tt.Run(\"for %s\", func (t *testing.T) {\n",
spec.AsErrnoName())
filePrintf(filep, "\t\tif v := classifySyscallError(%s); v != %s {\n",
spec.AsErrnoName(), spec.AsFailureVar())
filePrintf(filep, "\t\tt.Fatalf(\"expected '%%s', got '%%s'\", %s, v)\n",
filePrintf(filep, "\t\t\tt.Fatalf(\"expected '%%s', got '%%s'\", %s, v)\n",
spec.AsFailureVar())
fileWrite(filep, "\t}\n")
fileWrite(filep, "\t\t}\n")
fileWrite(filep, "\t})\n\n")
}
fileWrite(filep, "\tif v := classifySyscallError(syscall.Errno(0)); v != \"\" {\n")
fileWrite(filep, "\t\tt.Fatalf(\"expected empty string, got '%s'\", v)\n")
fileWrite(filep, "\t}\n")
fileWrite(filep, "\tt.Run(\"for the zero errno value\", func (t *testing.T) {\n")
fileWrite(filep, "\t\tif v := classifySyscallError(syscall.Errno(0)); v != \"\" {\n")
fileWrite(filep, "\t\t\tt.Fatalf(\"expected empty string, got '%s'\", v)\n")
fileWrite(filep, "\t\t}\n")
fileWrite(filep, "\t})\n")
fileWrite(filep, "}\n")
fileClose(filep)

View File

@ -1,6 +1,7 @@
package errorsx
// Operations that we measure.
// Operations that we measure. They are the possibly values of
// the ErrWrapper.Operation field.
const (
// ResolveOperation is the operation where we resolve a domain name.
ResolveOperation = "resolve"