#[path = "support/runtime.rs"]
mod runtime_support;
mod support;
use rsaeb::error::LimitError;
use rsaeb::execution::{
AppliedStep, FailedRun, OwnedStepTransition, ReturnedRun, RunSession, StableRun, StepTransition,
};
use rsaeb::input::RunSeed;
use rsaeb::limits::{
DEFAULT_MAX_INPUT_LEN, DEFAULT_MAX_RETURN_LEN, DEFAULT_MAX_STATE_LEN, ReturnByteLimit,
StepLimit,
};
use rsaeb::program::{RunOutcome, RunResult};
use runtime_support::TestRunPolicy;
use support::{TestFailure, TestResult, ensure_eq, ensure_matches, parse_program};
fn expect_stable_bytes(result: &RunResult, expected: &[u8]) -> TestResult {
match result.outcome() {
RunOutcome::Stable(output) if output.as_slice() == expected => Ok(()),
RunOutcome::Stable(_) => Err(TestFailure::message("stable output bytes differed")),
RunOutcome::Return(_) => Err(TestFailure::message("expected stable outcome")),
}
}
fn runtime_view_bytes(state: rsaeb::trace::RuntimeStateView<'_>) -> Result<Vec<u8>, TestFailure> {
Ok(state.materialize()?.into_raw_bytes())
}
#[derive(Debug, PartialEq, Eq)]
enum StepSignature {
Applied {
step: usize,
rule_position: usize,
state: Vec<u8>,
},
Stable {
steps: usize,
state: Vec<u8>,
},
Return {
step: usize,
rule_position: usize,
output: Vec<u8>,
},
}
fn applied_signature(applied: &AppliedStep<'_>) -> Result<StepSignature, TestFailure> {
Ok(StepSignature::Applied {
step: applied.step().get(),
rule_position: applied.rule_position().number().get(),
state: runtime_view_bytes(applied.state())?,
})
}
fn stable_signature(stable: &StableRun<'_>) -> Result<StepSignature, TestFailure> {
Ok(StepSignature::Stable {
steps: stable.steps().get(),
state: runtime_view_bytes(stable.state())?,
})
}
fn returned_signature(returned: &ReturnedRun<'_>) -> Result<StepSignature, TestFailure> {
Ok(StepSignature::Return {
step: returned.step().get(),
rule_position: returned.rule_position().number().get(),
output: returned.output().as_slice().to_vec(),
})
}
fn finish_step_signatures(
mut execution: RunSession<'_>,
) -> Result<Vec<StepSignature>, TestFailure> {
let mut signatures = Vec::new();
loop {
match expect_step_transition(execution.step())? {
StepTransition::Applied(applied) => {
signatures.push(applied_signature(&applied)?);
execution = applied.into_session();
}
StepTransition::Stable(stable) => {
signatures.push(stable_signature(&stable)?);
return Ok(signatures);
}
StepTransition::Returned(returned) => {
signatures.push(returned_signature(&returned)?);
return Ok(signatures);
}
StepTransition::Failed(failed) => return Err(TestFailure::from(failed.into_error())),
}
}
}
fn expect_step_transition<'program>(
result: StepTransition<'program>,
) -> Result<StepTransition<'program>, TestFailure> {
match result {
StepTransition::Failed(failed) => Err(TestFailure::from(failed.into_error())),
transition => Ok(transition),
}
}
fn expect_failed_transition<'program>(
result: StepTransition<'program>,
) -> Result<FailedRun<'program>, TestFailure> {
match result {
StepTransition::Failed(failed) => Ok(failed),
StepTransition::Applied(_) | StepTransition::Stable(_) | StepTransition::Returned(_) => {
Err(TestFailure::message("expected failed step"))
}
}
}
fn runtime_input(bytes: &[u8], limits: TestRunPolicy) -> Result<RunSeed, TestFailure> {
runtime_support::run_seed(bytes, limits)
}
#[test]
fn execution_rewrite_semantics_follow_public_contract() -> TestResult {
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(10_000),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let program = parse_program("aa=x\na=y")?;
let result = program.run(runtime_input(b"aaaa", limits)?)?;
expect_stable_bytes(&result, b"xx")?;
let program = parse_program("(start)a=x")?;
let result = program.run(runtime_input(b"aba", limits)?)?;
expect_stable_bytes(&result, b"xba")?;
let program = parse_program("(end)a=x")?;
let result = program.run(runtime_input(b"aba", limits)?)?;
expect_stable_bytes(&result, b"abx")?;
let program = parse_program("(once)a=b\na=c")?;
let result = program.run(runtime_input(b"aa", limits)?)?;
expect_stable_bytes(&result, b"bc")?;
let program = parse_program("ab=x")?;
let result = program.run(runtime_input(b"a=b", limits)?)?;
expect_stable_bytes(&result, b"a=b")?;
let program = parse_program("a= b")?;
let result = program.run(runtime_input(b"a bc", limits)?)?;
expect_stable_bytes(&result, b"b bc")
}
#[test]
fn execution_stepwise_transition_surface_is_rule_by_rule() -> TestResult {
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(10),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let program = parse_program("a=b\nb=c")?;
let input = runtime_input(b"a", limits)?;
let execution = program.start_run(input)?;
ensure_eq!(execution.completed_steps().get(), 0)?;
let execution = match expect_step_transition(execution.step())? {
StepTransition::Applied(applied) => {
ensure_eq!(applied.step().get(), 1)?;
ensure_eq!(applied.rule_position().number().get(), 1)?;
ensure_eq!(
runtime_view_bytes(applied.state())?.as_slice(),
b"b".as_slice()
)?;
ensure_eq!(applied.state().byte_count().get(), 1)?;
applied.into_session()
}
StepTransition::Stable(_) | StepTransition::Returned(_) | StepTransition::Failed(_) => {
return Err(TestFailure::message("expected first applied step"));
}
};
let execution = match expect_step_transition(execution.step())? {
StepTransition::Applied(applied) => {
ensure_eq!(applied.step().get(), 2)?;
ensure_eq!(applied.rule_position().number().get(), 2)?;
ensure_eq!(
runtime_view_bytes(applied.state())?.as_slice(),
b"c".as_slice()
)?;
applied.into_session()
}
StepTransition::Stable(_) | StepTransition::Returned(_) | StepTransition::Failed(_) => {
return Err(TestFailure::message("expected second applied step"));
}
};
match expect_step_transition(execution.step())? {
StepTransition::Stable(stable) => {
ensure_eq!(stable.steps().get(), 2)?;
ensure_eq!(
runtime_view_bytes(stable.state())?.as_slice(),
b"c".as_slice()
)?;
}
StepTransition::Applied(_) | StepTransition::Returned(_) | StepTransition::Failed(_) => {
return Err(TestFailure::message("expected stable completion"));
}
}
Ok(())
}
#[test]
fn execution_state_view_exposes_initial_and_current_state() -> TestResult {
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(10),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let program = parse_program("a=b")?;
let input = runtime_input(b"a", limits)?;
let execution = program.start_run(input)?;
ensure_eq!(
runtime_view_bytes(execution.state())?.as_slice(),
b"a".as_slice(),
)?;
let execution = match expect_step_transition(execution.step())? {
StepTransition::Applied(applied) => {
ensure_eq!(
runtime_view_bytes(applied.state())?.as_slice(),
b"b".as_slice()
)?;
applied.into_session()
}
StepTransition::Stable(_) | StepTransition::Returned(_) | StepTransition::Failed(_) => {
return Err(TestFailure::message("expected applied step"));
}
};
ensure_eq!(
runtime_view_bytes(execution.state())?.as_slice(),
b"b".as_slice(),
)
}
#[test]
fn execution_consumes_runtime_input_without_session_leakage() -> TestResult {
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(10),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let source = "(once)a=b\na=c";
let program = parse_program(source)?;
let first = program.start_run(runtime_input(b"aa", limits)?)?;
let second = program.start_run(runtime_input(b"aa", limits)?)?;
let third = program.start_run(runtime_input(b"aa", limits)?)?;
ensure_eq!(
finish_step_signatures(first)?,
[
StepSignature::Applied {
step: 1,
rule_position: 1,
state: b"ba".to_vec(),
},
StepSignature::Applied {
step: 2,
rule_position: 2,
state: b"bc".to_vec(),
},
StepSignature::Stable {
steps: 2,
state: b"bc".to_vec(),
},
],
)?;
ensure_eq!(
finish_step_signatures(second)?,
finish_step_signatures(third)?,
)
}
#[test]
fn execution_borrowed_run_and_owned_session_share_contract() -> TestResult {
let source = "a=b\nb=(return)ok";
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(10),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let borrowed = parse_program(source)?.run(runtime_input(b"a", limits)?)?;
let owned = parse_program(source)?
.into_run(runtime_input(b"a", limits)?)?
.finish()?;
ensure_eq!(borrowed, owned)
}
#[test]
fn execution_owned_terminals_can_return_program() -> TestResult {
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(10),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let stable_session = parse_program("a=b")?.into_run(runtime_input(b"a", limits)?)?;
let stable_session = match stable_session.step() {
OwnedStepTransition::Applied(applied) => applied.into_session(),
OwnedStepTransition::Stable(_)
| OwnedStepTransition::Returned(_)
| OwnedStepTransition::Failed(_) => {
return Err(TestFailure::message("expected applied owned step"));
}
};
let stable_program = match stable_session.step() {
OwnedStepTransition::Stable(stable) => stable.into_program(),
OwnedStepTransition::Applied(_)
| OwnedStepTransition::Returned(_)
| OwnedStepTransition::Failed(_) => {
return Err(TestFailure::message("expected owned stable terminal"));
}
};
ensure_eq!(stable_program.rule_count().get(), 1)?;
let returned_program = match parse_program("a=(return)ok")?
.into_run(runtime_input(b"a", limits)?)?
.step()
{
OwnedStepTransition::Returned(returned) => returned.into_program(),
OwnedStepTransition::Applied(_)
| OwnedStepTransition::Stable(_)
| OwnedStepTransition::Failed(_) => {
return Err(TestFailure::message("expected owned return terminal"));
}
};
ensure_eq!(returned_program.rule_count().get(), 1)?;
let failed_limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(1),
DEFAULT_MAX_STATE_LEN,
ReturnByteLimit::new(1),
);
let (error, failed_session) = match parse_program("a=(return)ok")?
.into_run(runtime_input(b"a", failed_limits)?)?
.step()
{
OwnedStepTransition::Failed(failed) => failed.into_parts(),
OwnedStepTransition::Applied(_)
| OwnedStepTransition::Stable(_)
| OwnedStepTransition::Returned(_) => {
return Err(TestFailure::message("expected owned failed terminal"));
}
};
ensure_matches(
matches!(
error,
rsaeb::error::RunError::Limit(LimitError::Return { .. })
),
"expected owned return limit failure",
)?;
ensure_eq!(failed_session.into_program().rule_count().get(), 1)
}
#[test]
fn execution_step_failure_is_terminal_transition() -> TestResult {
let program = parse_program("a=(return)ok")?;
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(1),
DEFAULT_MAX_STATE_LEN,
ReturnByteLimit::new(1),
);
let execution = program.start_run(runtime_input(b"a", limits)?)?;
let failed = expect_failed_transition(execution.step())?;
ensure_eq!(failed.completed_steps().get(), 0)?;
ensure_eq!(
runtime_view_bytes(failed.state())?.as_slice(),
b"a".as_slice(),
)?;
ensure_matches(
matches!(
failed.error(),
rsaeb::error::RunError::Limit(LimitError::Return {
limit,
attempted_len,
}) if limit == &ReturnByteLimit::new(1) && attempted_len.get() == 2
),
"expected return limit failure",
)
}
#[test]
fn execution_step_failure_preserves_current_progress() -> TestResult {
let program = parse_program("a=b\nb=c")?;
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(1),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let execution = program.start_run(runtime_input(b"a", limits)?)?;
let running = match expect_step_transition(execution.step())? {
StepTransition::Applied(applied) => applied.into_session(),
StepTransition::Stable(_) | StepTransition::Returned(_) | StepTransition::Failed(_) => {
return Err(TestFailure::message("expected applied execution"));
}
};
let failed = expect_failed_transition(running.step())?;
ensure_eq!(failed.completed_steps().get(), 1)?;
ensure_eq!(
runtime_view_bytes(failed.state())?.as_slice(),
b"b".as_slice(),
)?;
ensure_matches(
matches!(
failed.into_error(),
rsaeb::error::RunError::Limit(LimitError::Step {
completed_steps,
..
}) if completed_steps.get() == 1
),
"expected completed-step limit failure",
)
}