mod support;
use rsaeb::error::{LimitError, RunError, StateLimitContext};
use rsaeb::execution::{
AppliedExecution, ExecutionStepError, ExecutionTransition, ReturnedExecution, RunningExecution,
StableExecution,
};
use rsaeb::limits::{
DEFAULT_MAX_INPUT_LEN, DEFAULT_MAX_RETURN_LEN, DEFAULT_MAX_STATE_LEN, ReturnByteLimit,
RuntimeStateByteLimit, StepLimit,
};
use rsaeb::{RunLimits, RunOutcome, RunResult, RuntimeInput};
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_bytes() == 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<'_>) -> Vec<u8> {
state.bytes().collect()
}
#[derive(Debug, PartialEq, Eq)]
enum StepSignature {
Applied {
step: usize,
rule: Vec<u8>,
state: Vec<u8>,
},
Stable {
steps: usize,
state: Vec<u8>,
},
Return {
step: usize,
rule: Vec<u8>,
output: Vec<u8>,
},
}
fn applied_signature(applied: &AppliedExecution<'_>) -> Result<StepSignature, TestFailure> {
Ok(StepSignature::Applied {
step: applied.step().get(),
rule: applied.rule().canonical_source()?,
state: runtime_view_bytes(applied.state()),
})
}
fn stable_signature(stable: &StableExecution<'_>) -> StepSignature {
StepSignature::Stable {
steps: stable.steps().get(),
state: runtime_view_bytes(stable.state()),
}
}
fn returned_signature(returned: &ReturnedExecution<'_>) -> Result<StepSignature, TestFailure> {
Ok(StepSignature::Return {
step: returned.step().get(),
rule: returned.rule().canonical_source()?,
output: returned.output().to_vec()?,
})
}
fn finish_step_signatures(
mut execution: RunningExecution<'_>,
) -> Result<Vec<StepSignature>, TestFailure> {
let mut signatures = Vec::new();
loop {
match expect_step_transition(execution.step())? {
ExecutionTransition::Applied(applied) => {
signatures.push(applied_signature(&applied)?);
execution = applied.into_running();
}
ExecutionTransition::Stable(stable) => {
signatures.push(stable_signature(&stable));
return Ok(signatures);
}
ExecutionTransition::Returned(returned) => {
signatures.push(returned_signature(&returned)?);
return Ok(signatures);
}
}
}
}
fn expect_step_transition<'program>(
result: Result<ExecutionTransition<'program>, ExecutionStepError<'program>>,
) -> Result<ExecutionTransition<'program>, TestFailure> {
match result {
Ok(transition) => Ok(transition),
Err(error) => Err(TestFailure::from(error.into_error())),
}
}
fn expect_step_error<'program>(
result: Result<ExecutionTransition<'program>, ExecutionStepError<'program>>,
) -> Result<ExecutionStepError<'program>, TestFailure> {
match result {
Ok(_) => Err(TestFailure::message("expected step error")),
Err(error) => Ok(error),
}
}
fn runtime_input(bytes: &[u8]) -> Result<RuntimeInput, rsaeb::error::RuntimeInputError> {
RuntimeInput::validate(bytes, DEFAULT_MAX_INPUT_LEN)
}
#[test]
fn execution_rewrite_semantics_follow_public_contract() -> TestResult {
let limits = RunLimits::new(
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 = RunLimits::new(
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")?;
let execution = program.start_execution(&input, limits)?;
ensure_eq!(execution.completed_steps().get(), 0)?;
let execution = match expect_step_transition(execution.step())? {
ExecutionTransition::Applied(applied) => {
ensure_eq!(applied.step().get(), 1)?;
ensure_eq!(
applied.rule().canonical_source()?.as_slice(),
b"a=b".as_slice()
)?;
ensure_eq!(
runtime_view_bytes(applied.state()).as_slice(),
b"b".as_slice()
)?;
ensure_eq!(applied.state().byte_count().get(), 1)?;
applied.into_running()
}
ExecutionTransition::Stable(_) | ExecutionTransition::Returned(_) => {
return Err(TestFailure::message("expected first applied step"));
}
};
let execution = match expect_step_transition(execution.step())? {
ExecutionTransition::Applied(applied) => {
ensure_eq!(applied.step().get(), 2)?;
ensure_eq!(
applied.rule().canonical_source()?.as_slice(),
b"b=c".as_slice()
)?;
ensure_eq!(
runtime_view_bytes(applied.state()).as_slice(),
b"c".as_slice()
)?;
applied.into_running()
}
ExecutionTransition::Stable(_) | ExecutionTransition::Returned(_) => {
return Err(TestFailure::message("expected second applied step"));
}
};
match expect_step_transition(execution.step())? {
ExecutionTransition::Stable(stable) => {
ensure_eq!(stable.steps().get(), 2)?;
ensure_eq!(
runtime_view_bytes(stable.state()).as_slice(),
b"c".as_slice()
)?;
}
ExecutionTransition::Applied(_) | ExecutionTransition::Returned(_) => {
return Err(TestFailure::message("expected stable completion"));
}
}
Ok(())
}
#[test]
fn execution_state_view_exposes_initial_and_current_state() -> TestResult {
let limits = RunLimits::new(
StepLimit::new(10),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let program = parse_program("a=b")?;
let input = runtime_input(b"a")?;
let execution = program.start_execution(&input, limits)?;
ensure_eq!(
runtime_view_bytes(execution.state()).as_slice(),
b"a".as_slice(),
)?;
let execution = match expect_step_transition(execution.step())? {
ExecutionTransition::Applied(applied) => {
ensure_eq!(
runtime_view_bytes(applied.state()).as_slice(),
b"b".as_slice()
)?;
applied.into_running()
}
ExecutionTransition::Stable(_) | ExecutionTransition::Returned(_) => {
return Err(TestFailure::message("expected applied step"));
}
};
ensure_eq!(
runtime_view_bytes(execution.state()).as_slice(),
b"b".as_slice(),
)
}
#[test]
fn execution_reuses_runtime_input_without_session_leakage() -> TestResult {
let limits = RunLimits::new(
StepLimit::new(10),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let program = parse_program("(once)a=b\na=c")?;
let input = runtime_input(b"aa")?;
let first = program.start_execution(&input, limits)?;
let second = program.start_execution(&input, limits)?;
ensure_eq!(
finish_step_signatures(first)?,
[
StepSignature::Applied {
step: 1,
rule: b"(once)a=b".to_vec(),
state: b"ba".to_vec(),
},
StepSignature::Applied {
step: 2,
rule: b"a=c".to_vec(),
state: b"bc".to_vec(),
},
StepSignature::Stable {
steps: 2,
state: b"bc".to_vec(),
},
],
)?;
ensure_eq!(
finish_step_signatures(second)?,
finish_step_signatures(program.start_execution(&input, limits)?)?,
)?;
ensure_matches(
input.byte_count().get() == 2,
"expected reusable input size",
)
}
#[test]
fn execution_step_error_can_retry_with_relaxed_limits() -> TestResult {
let program = parse_program("a=(return)ok")?;
let input = runtime_input(b"a")?;
let limits = RunLimits::new(
StepLimit::new(1),
DEFAULT_MAX_STATE_LEN,
ReturnByteLimit::new(1),
);
let execution = program.start_execution(&input, limits)?;
let error = expect_step_error(execution.step())?;
let execution = error
.into_execution()
.with_limits(limits.with_return_byte_limit(ReturnByteLimit::new(2)))?;
match expect_step_transition(execution.step())? {
ExecutionTransition::Returned(returned) => {
ensure_eq!(returned.output().to_vec()?.as_slice(), b"ok".as_slice())
}
ExecutionTransition::Applied(_) | ExecutionTransition::Stable(_) => {
Err(TestFailure::message("expected returned execution"))
}
}
}
#[test]
fn execution_rejects_replacement_limits_that_do_not_fit_current_progress() -> TestResult {
let program = parse_program("a=b\nb=c")?;
let input = runtime_input(b"a")?;
let limits = RunLimits::new(
StepLimit::new(10),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let execution = program.start_execution(&input, limits)?;
let running = match expect_step_transition(execution.step())? {
ExecutionTransition::Applied(applied) => applied.into_running(),
ExecutionTransition::Stable(_) | ExecutionTransition::Returned(_) => {
return Err(TestFailure::message("expected applied execution"));
}
};
ensure_matches(
matches!(
running.with_limits(limits.with_step_limit(StepLimit::new(0))),
Err(RunError::Limit(LimitError::Step {
completed_steps,
..
})) if completed_steps.get() == 1
),
"expected completed-step replacement limit error",
)?;
let execution = program.start_execution(&input, limits)?;
ensure_matches(
matches!(
execution.with_limits(
limits.with_state_byte_limit(RuntimeStateByteLimit::new(0))
),
Err(RunError::Limit(LimitError::State {
context: StateLimitContext::CurrentState,
attempted_len,
..
})) if attempted_len.get() == 1
),
"expected current-state replacement limit error",
)
}