hyperfile 0.12.0

A file-like interface abstraction layer provides random read and write capabilities for S3
Documentation
//! The error classification, used from outside the crate as a caller does.
//!
//! A caller that also talks to S3 itself classifies its own errors with these, so
//! that both kinds of failure read by one rule. Built from SDK errors made here, so
//! no bucket is needed.

use std::io::ErrorKind;

use aws_sdk_s3::config::http::HttpResponse;
use aws_sdk_s3::error::{ErrorMetadata, SdkError};
use aws_sdk_s3::operation::get_object::GetObjectError;
use aws_smithy_types::body::SdkBody;

fn service_error(status: u16, code: &str) -> SdkError<GetObjectError, HttpResponse> {
    let err = GetObjectError::generic(ErrorMetadata::builder().code(code).message("made by the test").build());
    let raw = HttpResponse::new(status.try_into().unwrap(), SdkBody::empty());
    SdkError::service_error(err, raw)
}

/// Each code a DeleteObjects can report per key has the kind its status has — the
/// same failure reads the same whichever way S3 reported it.
#[test]
fn a_code_has_the_kind_of_its_status() {
    let pairs = [
        (403, "AccessDenied"), (403, "InvalidAccessKeyId"), (403, "SignatureDoesNotMatch"),
        (404, "NoSuchKey"), (404, "NoSuchBucket"),
        (409, "OperationAborted"), (412, "PreconditionFailed"), (409, "ConditionalRequestConflict"),
        (503, "SlowDown"), (500, "InternalError"), (503, "ServiceUnavailable"),
        (400, "RequestTimeout"), (400, "InvalidArgument"), (400, "MalformedXML"), (501, "NotImplemented"),
    ];
    for (status, code) in pairs {
        let by_status = hyperfile::s3_error_kind(&service_error(status, code));
        let by_code = hyperfile::s3_code_kind(Some(code));
        // RequestTimeout comes as 400; by status alone that is a bad request.
        if code == "RequestTimeout" {
            assert_eq!(by_code, ErrorKind::TimedOut);
            continue;
        }
        assert_eq!(by_code, by_status, "{} {}: by code {:?}, by status {:?}", status, code, by_code, by_status);
    }
    assert_eq!(hyperfile::s3_code_kind(Some("SomethingNew")), ErrorKind::Other);
    assert_eq!(hyperfile::s3_code_kind(None), ErrorKind::Other);
}

/// A refused condition is told by its code as well as its status: a
/// CompleteMultipartUpload reports one as 200 with the error in the body.
#[test]
fn a_refused_condition_is_told_by_its_code() {
    assert!(hyperfile::is_condition_failure(&service_error(412, "PreconditionFailed")));
    assert!(hyperfile::is_condition_failure(&service_error(200, "PreconditionFailed")));
    assert_eq!(hyperfile::s3_error_kind(&service_error(200, "PreconditionFailed")), ErrorKind::AlreadyExists);
    assert!(!hyperfile::is_condition_failure(&service_error(503, "SlowDown")));
}

/// Errors with no response: a timeout and a request never sent.
#[test]
fn errors_without_a_response() {
    let timeout: SdkError<GetObjectError, HttpResponse> = SdkError::timeout_error("made by the test");
    assert_eq!(hyperfile::s3_error_kind(&timeout), ErrorKind::TimedOut);
    let unbuilt: SdkError<GetObjectError, HttpResponse> = SdkError::construction_failure("made by the test");
    assert_eq!(hyperfile::s3_error_kind(&unbuilt), ErrorKind::InvalidInput);
}

/// The retry column of the table on `s3_error_kind`.
#[test]
fn which_kinds_are_retryable() {
    for k in [ErrorKind::ConnectionReset, ErrorKind::TimedOut, ErrorKind::ResourceBusy] {
        assert!(hyperfile::is_retryable(k), "{:?}", k);
    }
    for k in [ErrorKind::AlreadyExists, ErrorKind::NotFound, ErrorKind::PermissionDenied,
              ErrorKind::InvalidInput, ErrorKind::InvalidData, ErrorKind::Unsupported,
              ErrorKind::StaleNetworkFileHandle, ErrorKind::Other, ErrorKind::Interrupted] {
        assert!(!hyperfile::is_retryable(k), "{:?}", k);
    }
    // The statuses a store answers when busy, through the whole chain.
    for status in [429, 500, 502, 503, 504] {
        assert!(hyperfile::is_retryable(hyperfile::s3_error_kind(&service_error(status, "Busy"))), "{}", status);
    }
}