rsaeb 0.9.0

A no_std + alloc interpreter for A=B ordered rewrite programs.
Documentation
//! Public error model contract tests.

#[path = "support/runtime.rs"]
mod runtime_support;
mod support;

use rsaeb::error::{
    ParseErrorKind, ParseErrorLocation, ParseInvariantError, ParseRepresentationError, PayloadKind,
    RunError, RunInvariantError, RuntimeInputInvariantError,
};
use rsaeb::input::{RunSeed, RuntimeInput, RuntimeInputSource};
use rsaeb::limits::{DEFAULT_MAX_INPUT_LEN, RuntimeInputLimits};
use runtime_support::TestRunPolicy;
use support::{TestFailure, TestResult, ensure_eq, ensure_matches, parse_program};

/// Returns the expected runtime error.
///
/// # Errors
///
/// Returns `TestFailure` if the result succeeds.
fn expect_run_error<T>(result: Result<T, RunError>) -> Result<RunError, TestFailure> {
    match result {
        Ok(_) => Err(TestFailure::message("expected runtime error")),
        Err(error) => Ok(error),
    }
}

/// Validates test bytes as runtime input.
///
/// # Errors
///
/// Returns `RuntimeInputError` if the bytes are not valid runtime input.
fn runtime_input(bytes: &[u8], limits: TestRunPolicy) -> Result<RunSeed, TestFailure> {
    runtime_support::run_seed(bytes, limits)
}

/// # Errors
///
/// Returns `TestFailure` if parse errors lose structured location or kind
/// information.
#[test]
fn errors_parse_location_and_kind_are_structured() -> TestResult {
    let Err(error) = parse_program("a=b=c") else {
        return Err(TestFailure::message("expected parse error"));
    };

    ensure_eq!(error.line().get(), 1)?;
    match error.location() {
        ParseErrorLocation::Position(position) => {
            ensure_eq!(position.line().get(), 1)?;
            ensure_eq!(position.column().get(), 4)?;
        }
        ParseErrorLocation::Line(_) => {
            return Err(TestFailure::message("expected positioned parse error"));
        }
    }
    ensure_matches(
        matches!(error.kind(), ParseErrorKind::MultipleEquals),
        "expected multiple-equals parse error",
    )
}

/// # Errors
///
/// Returns `TestFailure` if payload or modifier errors lose domain-specific
/// information.
#[test]
fn errors_payload_and_modifier_kinds_keep_domain_information() -> TestResult {
    let Err(error) = parse_program("a = b (") else {
        return Err(TestFailure::message("expected reserved syntax error"));
    };
    ensure_matches(
        matches!(
            error.kind(),
            ParseErrorKind::ReservedSyntaxInPayload {
                payload_kind: PayloadKind::RightSideData,
                ..
            }
        ),
        "expected right payload syntax error",
    )?;

    let Err(error) = parse_program("(start)(once)a=b") else {
        return Err(TestFailure::message("expected modifier order error"));
    };
    ensure_matches(
        matches!(
            error.kind(),
            ParseErrorKind::UnsupportedLeftModifierOrder { .. }
        ),
        "expected left modifier order error",
    )
}

/// # Errors
///
/// Returns `TestFailure` if display output no longer names the expected domain
/// contexts.
#[test]
fn errors_display_output_names_domain_contexts() -> TestResult {
    let Err(parse_error) = parse_program("a=b=c") else {
        return Err(TestFailure::message("expected parse error"));
    };
    ensure_eq!(
        parse_error.to_string(),
        "parse error at line 1, column 4: multiple '=' characters are not allowed",
    )?;

    let Err(input_error) = RuntimeInput::validate(
        RuntimeInputSource::from_bytes(&[0xff]),
        RuntimeInputLimits::new(DEFAULT_MAX_INPUT_LEN),
    ) else {
        return Err(TestFailure::message("expected input error"));
    };
    ensure_eq!(
        input_error.to_string(),
        "input error: non-ASCII byte 0xff at column 1",
    )?;

    let return_limits = TestRunPolicy::new(
        DEFAULT_MAX_INPUT_LEN,
        rsaeb::limits::StepLimit::new(1),
        rsaeb::limits::DEFAULT_MAX_STATE_LEN,
        rsaeb::limits::ReturnByteLimit::new(1),
    );
    let return_error = parse_program("a=(return)ok")?.run(runtime_input(b"a", return_limits)?);
    ensure_matches(
        matches!(
            expect_run_error(return_error)?,
            RunError::Limit(rsaeb::error::LimitError::Return { .. })
        ),
        "expected return limit error",
    )
}

/// # Errors
///
/// Returns `TestFailure` if newly exposed invariant/representation error
/// domains lose display output.
#[test]
fn errors_invariant_and_representation_subdomains_are_public() -> TestResult {
    ensure_eq!(
        ParseRepresentationError::RulePosition.to_string(),
        "rule position could not be represented",
    )?;
    ensure_eq!(
        ParseInvariantError::ValidatedPayloadWithoutBytes.to_string(),
        "validated payload witness did not carry validated bytes",
    )?;
    ensure_eq!(
        RuntimeInputInvariantError::MissingValidatedAsciiByte.to_string(),
        "validated runtime-input witness contained a non-ASCII byte",
    )?;

    let once_program = parse_program("(once)a=b")?;
    let once_rule = once_program
        .rules()
        .next()
        .ok_or(TestFailure::message("expected once rule"))?;
    let available_slots = parse_program("")?.once_rule_count();
    ensure_eq!(
        RunInvariantError::MissingOnceRuleState {
            rule: once_rule.position(),
            available_slots,
        }
        .to_string(),
        "runtime invariant failure: once rule 1 had no state slot among 0 available once slots",
    )
}