#![cfg(feature = "live-engine")]
use std::path::PathBuf;
use std::time::{Duration, Instant};
use rs_teststand::{
AdapterKeyName, ConflictHandler, Engine, Error, GetSeqFileOptions, SequenceFile, StepGroup,
UIMessageCode, pump_thread_messages,
};
const INSERT_IF_MISSING: i32 = 1;
const NO_OPTIONS: i32 = 0;
fn runnable_file(engine: &Engine) -> Result<SequenceFile, Error> {
let sequence_file = engine.new_sequence_file()?;
let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
for index in 0..3 {
let step = engine.new_step(AdapterKeyName::NoneAdapter.as_str(), "Statement")?;
step.set_name(&format!("Work {index}"))?;
step.as_property_object()?.set_val_string(
"TS.PostExpr",
INSERT_IF_MISSING,
&format!("Locals.Counter = {index}"),
)?;
main_sequence.insert_step(&step, index, StepGroup::Main)?;
}
main_sequence
.locals()?
.set_val_number("Counter", INSERT_IF_MISSING, 0.0)?;
Ok(sequence_file)
}
fn run_to_end(engine: &Engine, deadline: Duration) -> Result<bool, Error> {
let started = Instant::now();
while started.elapsed() < deadline {
if pump_thread_messages() {
return Ok(false);
}
while !engine.is_ui_message_queue_empty()? {
let message = engine.get_ui_message()?;
let ended = matches!(
UIMessageCode::from_bits(message.event()?),
Ok(UIMessageCode::EndExecution)
);
message.acknowledge()?;
if ended {
return Ok(true);
}
}
}
Ok(false)
}
#[test]
#[ignore = "requires a live engine"]
fn an_execution_identifies_itself_the_way_a_front_end_needs() -> Result<(), Error> {
let engine = Engine::new()?;
engine.set_ui_message_polling_enabled(true)?;
let sequence_file = runnable_file(&engine)?;
let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
let id = execution.id()?;
let display_name = execution.display_name()?;
let threads = execution.num_threads()?;
println!(" id={id}, name={display_name:?}, threads={threads}");
assert!(id > 0, "an execution should have a positive id");
assert!(!display_name.is_empty(), "a front end needs a name to show");
assert!(threads >= 1, "an execution always has at least one thread");
assert!(run_to_end(&engine, Duration::from_secs(20))?);
engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
Ok(())
}
#[test]
#[ignore = "requires a live engine"]
fn the_result_status_settles_once_the_run_is_over() -> Result<(), Error> {
let engine = Engine::new()?;
engine.set_ui_message_polling_enabled(true)?;
let sequence_file = runnable_file(&engine)?;
let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
assert!(run_to_end(&engine, Duration::from_secs(20))?);
let status = execution.result_status()?;
println!(" final status: {status:?}");
assert!(
!status.is_empty(),
"a finished execution should report a status"
);
assert!(
["Passed", "Done", "Failed", "Terminated", "Error"].contains(&status.as_str()),
"unexpected status {status:?} — worth reading, not a failure of the API"
);
engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
Ok(())
}
#[test]
#[ignore = "requires a live engine"]
fn an_execution_reports_the_file_it_is_running() -> Result<(), Error> {
let engine = Engine::new()?;
engine.set_ui_message_polling_enabled(true)?;
let path = std::env::temp_dir().join("rs_teststand_execution_probe.seq");
let path = path.to_string_lossy().into_owned();
let sequence_file = runnable_file(&engine)?;
sequence_file.save(&path)?;
let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
let reported = execution.sequence_file_path()?;
println!(" running: {reported}");
assert!(
reported.eq_ignore_ascii_case(&path),
"expected {path}, got {reported}"
);
assert!(run_to_end(&engine, Duration::from_secs(20))?);
engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
let _ = std::fs::remove_file(&path);
Ok(())
}
#[test]
#[ignore = "requires a live engine"]
fn timings_are_available_while_the_run_is_in_progress() -> Result<(), Error> {
let engine = Engine::new()?;
engine.set_ui_message_polling_enabled(true)?;
let sequence_file = runnable_file(&engine)?;
let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
assert!(run_to_end(&engine, Duration::from_secs(20))?);
let executing = execution.seconds_executing()?;
let suspended = execution.seconds_suspended()?;
println!(" executing={executing}s, suspended={suspended}s");
assert!(executing >= 0.0, "elapsed time cannot be negative");
assert!(
suspended >= 0.0,
"a run that never broke should report no suspended time"
);
engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
Ok(())
}
#[test]
#[ignore = "requires a live engine"]
fn a_thread_is_reachable_both_by_index_and_as_the_foreground_one() -> Result<(), Error> {
let engine = Engine::new()?;
engine.set_ui_message_polling_enabled(true)?;
let sequence_file = runnable_file(&engine)?;
let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
let by_index = execution.get_thread(0)?;
let foreground = execution.foreground_thread()?;
assert!(by_index.as_property_object().is_ok());
assert!(foreground.as_property_object().is_ok());
assert!(run_to_end(&engine, Duration::from_secs(20))?);
engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
Ok(())
}
#[test]
#[ignore = "requires a live engine"]
fn an_execution_exposes_its_own_property_tree_and_error_object() -> Result<(), Error> {
let engine = Engine::new()?;
engine.set_ui_message_polling_enabled(true)?;
let sequence_file = runnable_file(&engine)?;
let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
assert!(run_to_end(&engine, Duration::from_secs(20))?);
let error_object = execution.error_object()?;
let occurred = error_object.get_val_bool("Occurred", NO_OPTIONS)?;
println!(" error occurred: {occurred}");
assert!(!occurred, "this sequence does nothing that can fail");
assert!(execution.as_property_object().is_ok());
engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
Ok(())
}
#[test]
#[ignore = "requires a live engine"]
fn terminating_a_run_is_asked_for_rather_than_immediate() -> Result<(), Error> {
let engine = Engine::new()?;
engine.set_ui_message_polling_enabled(true)?;
let sequence_file = runnable_file(&engine)?;
let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
execution.terminate()?;
let ended = run_to_end(&engine, Duration::from_secs(20))?;
assert!(ended, "the execution should still report its end");
let status = execution.result_status()?;
println!(" status after terminate: {status:?}");
engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
Ok(())
}
#[test]
#[ignore = "requires a live engine"]
fn a_thread_identifies_itself_and_reaches_its_context() -> Result<(), Error> {
let engine = Engine::new()?;
engine.set_ui_message_polling_enabled(true)?;
let sequence_file = runnable_file(&engine)?;
let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
let thread = execution.foreground_thread()?;
let id = thread.id()?;
let unique = thread.unique_thread_id()?;
let name = thread.display_name()?;
let depth = thread.call_stack_size()?;
println!(" thread id={id}, unique={unique:?}, name={name:?}, stack={depth}");
assert!(
!unique.is_empty(),
"a host keys on the unique id across runs"
);
assert!(depth >= 1, "a running thread has at least one frame");
assert_eq!(
thread.execution()?.id()?,
execution.id()?,
"a thread should lead back to its own execution"
);
assert!(run_to_end(&engine, Duration::from_secs(20))?);
engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
Ok(())
}
#[test]
#[ignore = "requires a live engine"]
fn station_globals_outlive_a_run_but_file_globals_do_not() -> Result<(), Error> {
let engine = Engine::new()?;
engine.set_ui_message_polling_enabled(true)?;
let sequence_file = engine.new_sequence_file()?;
let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
sequence_file
.file_globals_default_values()?
.set_val_string("Marker", INSERT_IF_MISSING, "default")?;
let step = engine.new_step(AdapterKeyName::NoneAdapter.as_str(), "Statement")?;
step.set_name("Touch Globals")?;
step.as_property_object()?.set_val_string(
"TS.PostExpr",
INSERT_IF_MISSING,
"FileGlobals.Marker = \"changed by the run\"",
)?;
main_sequence.insert_step(&step, 0, StepGroup::Main)?;
let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
assert!(run_to_end(&engine, Duration::from_secs(20))?);
assert_eq!(
sequence_file
.file_globals_default_values()?
.get_val_string("Marker", NO_OPTIONS)?,
"default",
"a run must not write through to the file's stored defaults"
);
let globals = engine.globals()?;
globals.set_val_string("RsTestStandProbe", INSERT_IF_MISSING, "kept")?;
assert_eq!(
globals.get_val_string("RsTestStandProbe", NO_OPTIONS)?,
"kept"
);
globals.delete_sub_property("RsTestStandProbe", NO_OPTIONS)?;
let _ = execution.id()?;
engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
Ok(())
}
#[test]
#[ignore = "requires a live engine"]
fn every_control_member_reaches_the_member_it_names() -> Result<(), Error> {
let engine = Engine::new()?;
engine.set_ui_message_polling_enabled(true)?;
let sequence_file = runnable_file(&engine)?;
let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
execution.as_property_object()?;
execution.get_sequence_file()?;
execution.cancel_termination()?;
assert!(run_to_end(&engine, Duration::from_secs(20))?);
engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
Ok(())
}
#[test]
#[ignore = "requires a live engine"]
fn aborting_stops_a_run_without_its_cleanup() -> Result<(), Error> {
let engine = Engine::new()?;
engine.set_ui_message_polling_enabled(true)?;
let sequence_file = runnable_file(&engine)?;
let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
execution.abort()?;
assert!(
run_to_end(&engine, Duration::from_secs(20))?,
"an aborted run should still report its end"
);
println!(" status after abort: {:?}", execution.result_status()?);
engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
Ok(())
}
#[test]
#[ignore = "requires a live engine"]
fn suspending_takes_effect_before_a_resume_is_safe() -> Result<(), Error> {
let engine = Engine::new()?;
engine.set_ui_message_polling_enabled(true)?;
let sequence_file = runnable_file(&engine)?;
let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
let thread = execution.foreground_thread()?;
execution.suspend()?;
let deadline = Instant::now() + Duration::from_secs(2);
let mut suspended = false;
while Instant::now() < deadline {
let _ = pump_thread_messages();
if thread.externally_suspended()? {
suspended = true;
break;
}
while !engine.is_ui_message_queue_empty()? {
engine.get_ui_message()?.acknowledge()?;
}
}
println!(" suspend observed: {suspended}");
execution.resume()?;
assert!(
run_to_end(&engine, Duration::from_secs(20))?,
"the run should finish once resumed"
);
engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
Ok(())
}
#[test]
#[ignore = "requires a live engine"]
fn shutting_down_is_confirmed_by_the_engine_and_bounded() -> Result<(), Error> {
let engine = Engine::new()?;
let sequence_file = runnable_file(&engine)?;
let _execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
let started = Instant::now();
let confirmed = engine.shutdown(Duration::from_secs(30))?;
let waited = started.elapsed();
println!(" confirmed={confirmed} after {waited:?}");
assert!(
waited < Duration::from_secs(30),
"the wait must be bounded, not merely finite"
);
assert!(
confirmed,
"the engine should confirm shutdown for a run this simple"
);
Ok(())
}
#[test]
#[ignore = "requires a live engine"]
fn results_parse_from_a_sequence_file_authored_in_the_editor() -> Result<(), Error> {
let engine = Engine::new()?;
engine.set_ui_message_polling_enabled(true)?;
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("ResultListParse.seq");
assert!(
path.is_file(),
"the fixture is committed and must be present: {}",
path.display()
);
let sequence_file = engine.get_sequence_file_ex(
&path.to_string_lossy(),
GetSeqFileOptions::DO_NOT_RUN_LOAD_CALLBACK,
ConflictHandler::Error,
)?;
let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
assert!(run_to_end(&engine, Duration::from_secs(20))?);
let parsed = execution.result_list()?.parse()?;
for result in &parsed {
println!(
" {} ({}) -> {} {:?}",
result.name, result.step_type, result.status, result.value
);
}
assert!(
!parsed.is_empty(),
"an authored sequence should record something"
);
assert!(
parsed.iter().all(|result| !result.status.is_empty()),
"every recorded result carries a status"
);
engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
Ok(())
}