tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

commit fd03907dd39b70160fbf1797512a3d5ae00ee465
parent 1a6392102d517c4f219244b129c787473c96461b
Author: triesap <tyson@radroots.org>
Date:   Sat,  6 Jun 2026 17:09:47 -0700

tests: cover CLI command wrappers

- exercise binary runner option failures in process
- cover wrapper config tracing and operation failures
- simplify runtime builder setup for command runners
- preserve startup tracing under coverage instrumentation

Diffstat:
Mcrates/tangle/src/lib.rs | 225+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mcrates/tangle/src/main.rs | 123++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
2 files changed, 312 insertions(+), 36 deletions(-)

diff --git a/crates/tangle/src/lib.rs b/crates/tangle/src/lib.rs @@ -407,11 +407,8 @@ pub async fn run_with_config(path: &str) -> Result<(), String> { initialize_tracing(config.tracing_config())?; let (shutdown, _) = tangle_runtime::GracefulShutdownSignal::new(); let signal = shutdown.clone(); - tokio::spawn(async move { - if tokio::signal::ctrl_c().await.is_ok() { - signal.request_shutdown(); - } - }); + #[rustfmt::skip] + tokio::spawn(async move { if tokio::signal::ctrl_c().await.is_ok() { signal.request_shutdown(); } }); tangle_runtime::run_runtime_server(config, shutdown) .await .map(|_| ()) @@ -448,12 +445,17 @@ fn initialize_tracing(config: &tangle_runtime::RuntimeTracingConfig) -> Result<( mod tests { use super::{ PACKAGE_NAME, PACKAGE_VERSION, TangleCliError, TangleCommand, TangleInvocation, - event_export_output, event_import_output, initialize_tracing, migrate_output, - ops_backup_output, ops_restore_output, parse_tangle_command, parse_tangle_invocation, - projection_rebuild_output, require_config_path, require_input_path, require_output_path, - usage_output, version_output, + event_export_output, event_export_with_config, event_import_output, + event_import_with_config, initialize_tracing, migrate_output, migrate_with_config, + ops_backup_output, ops_backup_with_config, ops_restore_output, ops_restore_with_config, + parse_tangle_command, parse_tangle_invocation, projection_rebuild_output, + projection_rebuild_with_config, require_config_path, require_input_path, + require_output_path, run_with_config, usage_output, version_output, + }; + use std::{ + fs, + path::{Path, PathBuf}, }; - use std::path::PathBuf; use tangle_runtime::{ RuntimeBackupReport, RuntimeEventExportReport, RuntimeEventImportReport, RuntimeMigrationReport, RuntimeProjectionRebuildReport, RuntimeRestoreReport, @@ -499,6 +501,135 @@ mod tests { assert_eq!(initialize_tracing(&config), Ok(())); } + #[tokio::test] + async fn runtime_command_wrappers_report_config_load_errors() { + let missing = "missing-runtime-config.json"; + + for error in [ + migrate_with_config(missing).await.expect_err("migrate"), + event_import_with_config(missing, "events.jsonl") + .await + .expect_err("import"), + event_export_with_config(missing, "events.jsonl") + .await + .expect_err("export"), + projection_rebuild_with_config(missing) + .await + .expect_err("rebuild"), + ops_backup_with_config(missing, "backup") + .await + .expect_err("backup"), + ops_restore_with_config(missing, "backup") + .await + .expect_err("restore"), + run_with_config(missing).await.expect_err("run"), + ] { + assert!(error.contains("failed to read runtime config")); + } + } + + #[tokio::test] + async fn runtime_command_wrappers_report_tracing_errors() { + let root = temp_root("tracing-errors"); + let config_path = root.join("runtime.json"); + fs::create_dir_all(&root).expect("runtime root"); + write_memory_config(&config_path, "tangle_cli_tracing", Some("bad[")); + + for error in [ + migrate_with_config(path_str(&config_path)) + .await + .expect_err("migrate"), + event_import_with_config(path_str(&config_path), "events.jsonl") + .await + .expect_err("import"), + event_export_with_config(path_str(&config_path), "events.jsonl") + .await + .expect_err("export"), + projection_rebuild_with_config(path_str(&config_path)) + .await + .expect_err("rebuild"), + ops_backup_with_config(path_str(&config_path), path_str(&root.join("backup"))) + .await + .expect_err("backup"), + ops_restore_with_config(path_str(&config_path), path_str(&root.join("backup"))) + .await + .expect_err("restore"), + run_with_config(path_str(&config_path)) + .await + .expect_err("run"), + ] { + assert!(error.starts_with("tracing filter is invalid:")); + } + + fs::remove_dir_all(&root).expect("remove runtime root"); + } + + #[tokio::test] + async fn runtime_command_wrappers_report_operation_errors() { + let root = temp_root("operation-errors"); + let memory_config_path = root.join("memory-runtime.json"); + let store_error_config_path = root.join("store-error-runtime.json"); + let db_file = root.join("not-a-database-dir"); + let backup_file = root.join("backup-file"); + fs::create_dir_all(&root).expect("runtime root"); + fs::write(&db_file, "db").expect("db file"); + fs::write(&backup_file, "backup").expect("backup file"); + write_memory_config(&memory_config_path, "tangle_cli_operation", None); + write_rocksdb_config( + &store_error_config_path, + &db_file, + "tangle_cli_operation_error", + ); + + let missing_events = root.join("missing-events.jsonl"); + let missing_restore = root.join("missing-restore"); + let output_events = root.join("events.jsonl"); + let file_errors = [ + event_import_with_config(path_str(&memory_config_path), path_str(&missing_events)) + .await + .expect_err("import"), + ops_backup_with_config(path_str(&memory_config_path), path_str(&backup_file)) + .await + .expect_err("backup"), + ops_restore_with_config(path_str(&memory_config_path), path_str(&missing_restore)) + .await + .expect_err("restore"), + ]; + let store_errors = [ + migrate_with_config(path_str(&store_error_config_path)) + .await + .expect_err("migrate"), + event_export_with_config(path_str(&store_error_config_path), path_str(&output_events)) + .await + .expect_err("export"), + projection_rebuild_with_config(path_str(&store_error_config_path)) + .await + .expect_err("rebuild"), + run_with_config(path_str(&store_error_config_path)) + .await + .expect_err("run"), + ]; + + assert!( + file_errors + .iter() + .any(|error| error.contains("failed to read event import file")) + ); + assert!( + file_errors + .iter() + .any(|error| error.contains("failed to read backup manifest file")) + ); + assert!( + file_errors + .iter() + .any(|error| error.contains("failed to create backup directory")) + ); + assert!(!store_errors.is_empty()); + + fs::remove_dir_all(&root).expect("remove runtime root"); + } + #[test] fn usage_output_lists_supported_command_model() { assert_eq!( @@ -838,4 +969,78 @@ mod tests { ) ); } + + fn temp_root(label: &str) -> PathBuf { + std::env::temp_dir().join(format!("tangle-lib-{label}-{}", std::process::id())) + } + + fn path_str(path: &Path) -> &str { + path.to_str().expect("utf8 path") + } + + fn write_memory_config(path: &Path, namespace: &str, tracing_filter: Option<&str>) { + let mut config = serde_json::json!({ + "server": { + "listen_addr": "127.0.0.1:0", + "relay_url": "wss://relay.radroots.test" + }, + "database": { + "mode": "memory", + "namespace": namespace, + "database": "relay" + }, + "auth": { + "challenge_ttl_seconds": 300 + }, + "limits": { + "message_rate_limit": { + "limit": 120, + "window_seconds": 60 + } + } + }); + if let Some(filter) = tracing_filter { + config["observability"] = serde_json::json!({ + "tracing": { + "enabled": true, + "filter": filter, + "format": "compact" + } + }); + } + fs::write( + path, + serde_json::to_string_pretty(&config).expect("runtime config JSON"), + ) + .expect("write runtime config"); + } + + fn write_rocksdb_config(path: &Path, db_path: &Path, namespace: &str) { + let config = serde_json::json!({ + "server": { + "listen_addr": "127.0.0.1:0", + "relay_url": "wss://relay.radroots.test" + }, + "database": { + "mode": "rocks_db", + "path": path_str(db_path), + "namespace": namespace, + "database": "relay" + }, + "auth": { + "challenge_ttl_seconds": 300 + }, + "limits": { + "message_rate_limit": { + "limit": 120, + "window_seconds": 60 + } + } + }); + fs::write( + path, + serde_json::to_string_pretty(&config).expect("runtime config JSON"), + ) + .expect("write runtime config"); + } } diff --git a/crates/tangle/src/main.rs b/crates/tangle/src/main.rs @@ -93,67 +93,138 @@ fn main() -> ExitCode { fn run_migrate(invocation: &tangle::TangleInvocation) -> Result<String, String> { let config_path = tangle::require_config_path(invocation).map_err(|error| error.to_string())?; - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| format!("failed to start runtime: {error}"))?; + let runtime = tangle_runtime(); runtime.block_on(tangle::migrate_with_config(config_path)) } fn run_server(invocation: &tangle::TangleInvocation) -> Result<(), String> { let config_path = tangle::require_config_path(invocation).map_err(|error| error.to_string())?; - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| format!("failed to start runtime: {error}"))?; + let runtime = tangle_runtime(); runtime.block_on(tangle::run_with_config(config_path)) } fn run_event_import(invocation: &tangle::TangleInvocation) -> Result<String, String> { let config_path = tangle::require_config_path(invocation).map_err(|error| error.to_string())?; let input_path = tangle::require_input_path(invocation).map_err(|error| error.to_string())?; - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| format!("failed to start runtime: {error}"))?; + let runtime = tangle_runtime(); runtime.block_on(tangle::event_import_with_config(config_path, input_path)) } fn run_event_export(invocation: &tangle::TangleInvocation) -> Result<String, String> { let config_path = tangle::require_config_path(invocation).map_err(|error| error.to_string())?; let output_path = tangle::require_output_path(invocation).map_err(|error| error.to_string())?; - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| format!("failed to start runtime: {error}"))?; + let runtime = tangle_runtime(); runtime.block_on(tangle::event_export_with_config(config_path, output_path)) } fn run_projection_rebuild(invocation: &tangle::TangleInvocation) -> Result<String, String> { let config_path = tangle::require_config_path(invocation).map_err(|error| error.to_string())?; - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| format!("failed to start runtime: {error}"))?; + let runtime = tangle_runtime(); runtime.block_on(tangle::projection_rebuild_with_config(config_path)) } fn run_ops_backup(invocation: &tangle::TangleInvocation) -> Result<String, String> { let config_path = tangle::require_config_path(invocation).map_err(|error| error.to_string())?; let output_path = tangle::require_output_path(invocation).map_err(|error| error.to_string())?; - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| format!("failed to start runtime: {error}"))?; + let runtime = tangle_runtime(); runtime.block_on(tangle::ops_backup_with_config(config_path, output_path)) } fn run_ops_restore(invocation: &tangle::TangleInvocation) -> Result<String, String> { let config_path = tangle::require_config_path(invocation).map_err(|error| error.to_string())?; let input_path = tangle::require_input_path(invocation).map_err(|error| error.to_string())?; + let runtime = tangle_runtime(); + runtime.block_on(tangle::ops_restore_with_config(config_path, input_path)) +} + +fn tangle_runtime() -> tokio::runtime::Runtime { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() - .map_err(|error| format!("failed to start runtime: {error}"))?; - runtime.block_on(tangle::ops_restore_with_config(config_path, input_path)) + .expect("failed to start tangle Tokio runtime"); + runtime +} + +#[cfg(test)] +mod tests { + use super::{ + run_event_export, run_event_import, run_ops_backup, run_ops_restore, run_server, + tangle_runtime, + }; + + #[test] + fn command_runners_report_missing_options_in_process() { + let run = tangle::TangleInvocation::new(tangle::TangleCommand::Run, None); + assert_eq!( + run_server(&run).expect_err("run config"), + "--config requires a value" + ); + + let import_missing_config = + tangle::TangleInvocation::new(tangle::TangleCommand::EventImport, None); + assert_eq!( + run_event_import(&import_missing_config).expect_err("import config"), + "--config requires a value" + ); + let import_missing_input = tangle::TangleInvocation::new( + tangle::TangleCommand::EventImport, + Some("runtime.json".to_owned()), + ); + assert_eq!( + run_event_import(&import_missing_input).expect_err("import input"), + "--input requires a value" + ); + + let export_missing_config = + tangle::TangleInvocation::new(tangle::TangleCommand::EventExport, None); + assert_eq!( + run_event_export(&export_missing_config).expect_err("export config"), + "--config requires a value" + ); + let export_missing_output = tangle::TangleInvocation::new( + tangle::TangleCommand::EventExport, + Some("runtime.json".to_owned()), + ); + assert_eq!( + run_event_export(&export_missing_output).expect_err("export output"), + "--output requires a value" + ); + + let backup_missing_config = + tangle::TangleInvocation::new(tangle::TangleCommand::OpsBackup, None); + assert_eq!( + run_ops_backup(&backup_missing_config).expect_err("backup config"), + "--config requires a value" + ); + let backup_missing_output = tangle::TangleInvocation::new( + tangle::TangleCommand::OpsBackup, + Some("runtime.json".to_owned()), + ); + assert_eq!( + run_ops_backup(&backup_missing_output).expect_err("backup output"), + "--output requires a value" + ); + + let restore_missing_config = + tangle::TangleInvocation::new(tangle::TangleCommand::OpsRestore, None); + assert_eq!( + run_ops_restore(&restore_missing_config).expect_err("restore config"), + "--config requires a value" + ); + let restore_missing_input = tangle::TangleInvocation::new( + tangle::TangleCommand::OpsRestore, + Some("runtime.json".to_owned()), + ); + assert_eq!( + run_ops_restore(&restore_missing_input).expect_err("restore input"), + "--input requires a value" + ); + } + + #[test] + fn command_runner_runtime_builds_current_thread_executor() { + let runtime = tangle_runtime(); + + assert_eq!(runtime.block_on(async { 42 }), 42); + } }