Lines
95.42 %
Functions
100 %
Branches
//! `eval_form` driven through a real `rpc::Session` for every `RenderMode`.
//! List and report modes get the reply shape their natives produce as a string
//! literal (or `nil`), so no database is needed to reach each arm.
use rpc::{ScriptCtx, Session};
use super::*;
const REPORT: &str = r#""(:periods nil)""#;
async fn eval(form: &str, mode: RenderMode) -> Result<Rendered, CommandError> {
let mut session = Session::new(ScriptCtx::new(Uuid::nil())).expect("Session::new");
eval_form(&mut session, form, mode).await
}
async fn lines(form: &str, mode: RenderMode) -> Vec<String> {
match eval(form, mode).await {
Ok(Rendered::Lines(lines)) => lines,
Ok(Rendered::Bytes(_)) => panic!("{form}: rendered bytes"),
Ok(Rendered::Nothing) => panic!("{form}: rendered nothing"),
Err(e) => panic!("{form}: {e}"),
async fn error_message(form: &str) -> String {
match eval(form, RenderMode::Scalar).await {
Err(CommandError::Command(message)) => message,
Err(other) => panic!("{form}: unexpected error {other}"),
Ok(_) => panic!("{form}: expected a server error"),
fn report_lines(rendered: Result<Rendered, CommandError>) -> Vec<String> {
match rendered {
_ => panic!("report must render as text lines"),
#[tokio::test]
async fn eval_form_scalar_renders_numbers_and_strings() {
assert_eq!(lines("(+ 1 2)", RenderMode::Scalar).await, vec!["3"]);
assert_eq!(lines(r#""hello""#, RenderMode::Scalar).await, vec!["hello"]);
async fn eval_form_bool_success_decodes_t_nil_and_flags() {
assert_eq!(lines("t", RenderMode::BoolSuccess).await, vec!["true"]);
assert_eq!(lines("nil", RenderMode::BoolSuccess).await, vec!["false"]);
assert_eq!(lines("0", RenderMode::BoolSuccess).await, vec!["false"]);
assert_eq!(lines("1", RenderMode::BoolSuccess).await, vec!["true"]);
assert!(eval(r#""yes""#, RenderMode::BoolSuccess).await.is_err());
async fn eval_form_silent_renders_nothing() {
assert!(matches!(
eval("(+ 1 2)", RenderMode::Silent).await,
Ok(Rendered::Nothing)
));
async fn eval_form_config_value_extracts_string_bytes_and_unset() {
assert_eq!(
lines(r#""(:config-value \"x\")""#, RenderMode::ConfigValue).await,
vec!["x"]
);
eval(r#""(:config-value #u8(1 2))""#, RenderMode::ConfigValue).await,
Ok(Rendered::Bytes(ref b)) if b == &[1, 2]
eval(r#""(:config-value nil)""#, RenderMode::ConfigValue).await,
async fn eval_form_account_and_commodity_lists() {
lines(
r#""((:account :id \"a\" :name \"Cash\"))""#,
RenderMode::AccountList
)
.await,
vec!["Cash"]
assert!(lines("nil", RenderMode::AccountList).await.is_empty());
r#""((:commodity :id \"c\" :symbol \"USD\" :name \"US Dollar\"))""#,
RenderMode::CommodityList
vec!["USD - US Dollar"]
async fn eval_form_ssh_key_and_transaction_lists() {
r#""((:ssh-key :fingerprint \"SHA256:x\" :key-type \"ed25519\" :created-at \"2026-01-01\" :name \"laptop\"))""#,
RenderMode::SshKeyList
vec!["laptop [ed25519] SHA256:x created:2026-01-01 last-used:never"]
r#""((:transaction :id \"t-1\" :note \"lunch\" :post-date \"2026-06-27\"))""#,
RenderMode::TransactionList
vec!["lunch - 2026-06-27"]
assert!(lines("nil", RenderMode::TransactionList).await.is_empty());
async fn eval_form_empty_balance_list_prints_the_sentinel() {
lines("nil", RenderMode::BalanceList).await,
vec![NO_TRANSACTION_YET]
async fn eval_form_report_modes_render_text_charts() {
let report = Value::String("(:periods nil)".into());
let chart = "text".to_string();
REPORT,
RenderMode::ReportBalance {
chart: chart.clone()
report_lines(render_report_balance(&report, &chart))
RenderMode::ReportActivity {
report_lines(render_report_activity(&report, &chart))
RenderMode::ReportBreakdown {
report_lines(render_report_breakdown(&report, &chart))
async fn eval_form_server_errors_surface_as_command_errors() {
let raised = error_message(r#"(error 'boom "bad input")"#).await;
assert!(
raised.contains("boom") && raised.contains("bad input"),
"{raised}"
let undefined = error_message("(no-such-function)").await;
assert!(undefined.contains("[compile]"), "{undefined}");