use dataflow_rs::Engine;
use dataflow_rs::engine::{AsyncFunctionHandler, Message, Workflow};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
use swift_mt_message::plugin::register_swift_mt_functions;
#[derive(Debug, Serialize, Deserialize)]
struct ScenarioInfo {
file: String,
description: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct ScenarioIndex {
message_type: String,
description: String,
scenarios: Vec<ScenarioInfo>,
}
#[tokio::test]
async fn test_swift_mt_workflow_pipeline() {
let message_type = env::var("TEST_MESSAGE_TYPE").ok();
let scenario_name = env::var("TEST_SCENARIO").ok();
let debug_mode = env::var("TEST_DEBUG").is_ok();
let stop_on_failure = env::var("TEST_STOP_ON_FAILURE").is_ok();
let samples_str = env::var("TEST_SAMPLE_COUNT").unwrap_or_else(|_| "10".to_string());
let samples_per_scenario = samples_str.parse::<usize>().unwrap_or(10);
let mut custom_functions: HashMap<String, Box<dyn AsyncFunctionHandler + Send + Sync>> =
HashMap::new();
for (name, handler) in register_swift_mt_functions() {
custom_functions.insert(name.to_string(), handler);
}
let test_cases = get_test_cases(message_type.as_deref(), scenario_name.as_deref());
if test_cases.is_empty() {
panic!("No test cases found for the given parameters");
}
let mut workflows = Vec::new();
let mut processed_types = std::collections::HashSet::new();
for (mt_type, _, _) in &test_cases {
if processed_types.insert(mt_type.clone()) {
workflows.push(create_swift_mt_workflow(mt_type));
}
}
let engine = Engine::new(workflows, Some(custom_functions));
let mut all_results = Vec::new();
let mut failure_count = 0;
for (message_type, scenario, description) in &test_cases {
if debug_mode {
println!("\n========================================");
println!("Testing {} - {}", message_type, scenario);
if scenario != description {
println!("Description: {}", description);
}
println!("========================================");
}
for sample_idx in 0..samples_per_scenario {
let schema = match load_scenario_schema(message_type, scenario) {
Ok(schema) => schema,
Err(e) => {
if debug_mode {
eprintln!(
"Failed to load schema for {}/{}: {}",
message_type, scenario, e
);
}
failure_count += 1;
continue;
}
};
let mut message = Message::from_value(&schema);
message
.data_mut()
.as_object_mut()
.unwrap()
.insert("message_type".to_string(), json!(message_type));
message
.data_mut()
.as_object_mut()
.unwrap()
.insert("scenario".to_string(), json!(scenario));
message.invalidate_context_cache();
if debug_mode {
println!("\nDebug - Initial message created with:");
println!(" message_type: {}", message_type);
println!(" scenario: {}", scenario);
println!(
" payload (schema): {} bytes",
serde_json::to_string(&schema).unwrap_or_default().len()
);
if let Some(obj) = message.data().as_object() {
println!(
" Data fields present: {:?}",
obj.keys().collect::<Vec<_>>()
);
}
}
message.metadata_mut().as_object_mut().unwrap().insert(
"test_info".to_string(),
json!({
"message_type": message_type,
"scenario": scenario,
"sample_index": sample_idx,
"start_time": chrono::Utc::now().to_rfc3339()
}),
);
let result = engine.process_message(&mut message).await;
if debug_mode {
println!("\nDebug - Message data before processing:");
if let Some(obj) = message.data().as_object() {
for (key, _) in obj {
println!(" - {}", key);
}
}
println!("\nDebug - Workflow execution result: {:?}", result);
println!("\nDebug - Message data after processing:");
if let Some(obj) = message.data().as_object() {
println!(" Total fields in data: {}", obj.len());
if !obj.contains_key("validation_result") {
println!(" WARNING: validation_result field is missing!");
}
for (key, value) in obj {
println!(" - {}", key);
if (key == "sample_json"
|| key == "sample_mt"
|| key == "validation_result"
|| key == "mt_json")
&& (message_type == "MT104"
|| (scenario == "standard" && message_type == "MT103"))
{
println!("\nDebug - {} structure:", key);
if let Some(json_data) = value.get("json_data") {
if let Some(json_obj) = json_data.as_object() {
println!(
" json_data keys: {:?}",
json_obj.keys().collect::<Vec<_>>()
);
if let Some(fields) = json_obj.get("fields")
&& let Some(fields_obj) = fields.as_object()
{
println!(
" fields keys: {:?}",
fields_obj.keys().collect::<Vec<_>>()
);
if let Some(field_20) = fields_obj.get("20") {
println!(
" field 20 structure: {}",
serde_json::to_string_pretty(field_20)
.unwrap_or_default()
);
}
if let Some(field_50) = fields_obj.get("50") {
println!(
" field 50 structure: {}",
serde_json::to_string_pretty(field_50)
.unwrap_or_default()
);
}
}
} else {
println!(" json_data is not an object!");
}
} else {
println!(" No json_data field in {}!", key);
println!(
" {} keys: {:?}",
key,
value.as_object().map(|o| o.keys().collect::<Vec<_>>())
);
if message_type == "MT104" && key == "sample_json" {
if let Some(obj) = value.as_object() {
if let Some(fields) = obj.get("fields") {
println!(
" MT104 fields: {}",
serde_json::to_string_pretty(fields)
.unwrap_or_default()
);
}
}
}
}
} else if key == "sample_mt" {
println!("\nDebug - Sample MT structure:");
if let Some(mt_str) = value.get("mt_message").and_then(|v| v.as_str()) {
println!(" MT message exists, full message:");
println!(" {}", mt_str);
} else if let Some(mt_str) = value.as_str() {
println!(" MT message as string, full message:");
println!(" {}", mt_str);
} else {
println!(" MT message structure: {:?}", value);
}
} else if key == "validation_result" {
println!("\nDebug - Validation Result:");
if let Some(valid) = value.get("valid").and_then(|v| v.as_bool()) {
println!(" Valid: {}", valid);
}
if let Some(errors) = value.get("errors").and_then(|v| v.as_array()) {
println!(" Errors count: {}", errors.len());
for (i, err) in errors.iter().take(3).enumerate() {
println!(" Error {}: {:?}", i + 1, err);
}
}
}
}
if obj.is_empty() {
println!(
"\nWARNING: Message data is empty - workflow may not have been triggered!"
);
println!("Expected workflow condition: data.message_type == 'MT103'");
if let Some(mt) = obj.get("message_type") {
println!("Actual message_type value: {:?}", mt);
}
}
} else {
println!(" Message data is not an object!");
}
}
let test_result = match result {
Ok(_) => {
let workflow_result = analyze_workflow_results(&message, debug_mode);
TestResult {
message_type: message_type.to_string(),
scenario: scenario.to_string(),
workflow_completed: true,
generate_success: workflow_result.generate_success,
publish_success: workflow_result.publish_success,
parse_success: workflow_result.parse_success,
validation_passed: workflow_result.validate_success,
transform_success: workflow_result.transform_success,
round_trip_success: workflow_result.round_trip_match,
error: None,
}
}
Err(e) => {
if debug_mode {
eprintln!("\nWorkflow execution failed: {:?}", e);
}
TestResult {
message_type: message_type.to_string(),
scenario: scenario.to_string(),
workflow_completed: false,
generate_success: false,
publish_success: false,
parse_success: false,
validation_passed: false,
transform_success: false,
round_trip_success: false,
error: Some(format!("{:?}", e)),
}
}
};
if !test_result.is_fully_successful() {
failure_count += 1;
if debug_mode {
println!("\n❌ Sample {} failed:", sample_idx);
println!(" Workflow Steps:");
println!(
" 1. Generate: {}",
status_symbol(test_result.generate_success)
);
println!(
" 2. Publish: {}",
status_symbol(test_result.publish_success)
);
println!(" 3. Parse: {}", status_symbol(test_result.parse_success));
println!(
" 4. Validate: {}",
status_symbol(test_result.validation_passed)
);
println!(
" 5. Transform: {}",
status_symbol(test_result.transform_success)
);
println!(
" Round-trip: {}",
status_symbol(test_result.round_trip_success)
);
if let Some(ref error) = test_result.error {
println!(" Error: {}", error);
}
}
if stop_on_failure {
eprintln!("\n⛔ Stopping on first failure (TEST_STOP_ON_FAILURE=1)");
all_results.push(test_result);
break;
}
}
all_results.push(test_result);
}
if stop_on_failure && failure_count > 0 {
break;
}
}
print_test_summary(&all_results);
if failure_count > 0 {
panic!(
"\n❌ Workflow test failed: {} out of {} tests failed",
failure_count,
all_results.len()
);
} else {
println!("\n✅ All {} workflow tests passed!", all_results.len());
}
}
fn get_test_cases(
message_type: Option<&str>,
scenario: Option<&str>,
) -> Vec<(String, String, String)> {
match (message_type, scenario) {
(None, None) => {
get_all_test_cases()
}
(Some(mt), None) => {
let index_path = format!("test_scenarios/{}/index.json", mt.to_lowercase());
if let Ok(content) = fs::read_to_string(&index_path) {
if let Ok(index) = serde_json::from_str::<ScenarioIndex>(&content) {
index
.scenarios
.into_iter()
.map(|s| {
let name = s.file.trim_end_matches(".json");
(mt.to_string(), name.to_string(), s.description)
})
.collect()
} else {
get_scenarios_for_message_type(mt)
.into_iter()
.map(|s| (mt.to_string(), s.clone(), s))
.collect()
}
} else {
get_scenarios_for_message_type(mt)
.into_iter()
.map(|s| (mt.to_string(), s.clone(), s))
.collect()
}
}
(Some(mt), Some(sc)) => {
vec![(mt.to_string(), sc.to_string(), sc.to_string())]
}
(None, Some(_)) => {
eprintln!("Warning: TEST_SCENARIO requires TEST_MESSAGE_TYPE");
vec![]
}
}
}
fn get_all_test_cases() -> Vec<(String, String, String)> {
let mut test_cases = Vec::new();
let scenarios_dir = Path::new("test_scenarios");
if let Ok(entries) = fs::read_dir(scenarios_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir()
&& let Some(dir_name) = path.file_name().and_then(|s| s.to_str())
&& dir_name.starts_with("mt")
{
let message_type = dir_name.to_uppercase();
let index_path = path.join("index.json");
if index_path.exists()
&& let Ok(content) = fs::read_to_string(&index_path)
&& let Ok(index) = serde_json::from_str::<ScenarioIndex>(&content)
{
for scenario_info in index.scenarios {
let scenario_name = scenario_info.file.trim_end_matches(".json");
test_cases.push((
message_type.clone(),
scenario_name.to_string(),
scenario_info.description,
));
}
continue;
}
let scenarios = get_scenarios_fallback(&message_type);
for scenario in scenarios {
test_cases.push((message_type.clone(), scenario.clone(), scenario));
}
}
}
}
test_cases
}
fn get_scenarios_for_message_type(message_type: &str) -> Vec<String> {
let index_path = format!("test_scenarios/{}/index.json", message_type.to_lowercase());
match fs::read_to_string(&index_path) {
Ok(content) => match serde_json::from_str::<ScenarioIndex>(&content) {
Ok(index) => index
.scenarios
.into_iter()
.map(|s| {
s.file.trim_end_matches(".json").to_string()
})
.collect(),
Err(e) => {
eprintln!("Failed to parse index.json for {}: {}", message_type, e);
Vec::new()
}
},
Err(e) => {
eprintln!("Failed to read index.json for {}: {}", message_type, e);
get_scenarios_fallback(message_type)
}
}
}
fn get_scenarios_fallback(message_type: &str) -> Vec<String> {
let mut scenarios = Vec::new();
let scenario_dir = Path::new("test_scenarios").join(message_type.to_lowercase());
if let Ok(entries) = fs::read_dir(&scenario_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json")
&& let Some(stem) = path.file_stem().and_then(|s| s.to_str())
&& stem != "index"
{
scenarios.push(stem.to_string());
}
}
}
scenarios
}
fn load_scenario_schema(message_type: &str, scenario: &str) -> Result<Value, String> {
let file_path = format!(
"test_scenarios/{}/{}.json",
message_type.to_lowercase(),
scenario
);
fs::read_to_string(&file_path)
.map_err(|e| format!("Failed to read file {}: {}", file_path, e))
.and_then(|content| {
serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse JSON from {}: {}", file_path, e))
})
}
fn create_swift_mt_workflow(message_type: &str) -> Workflow {
let workflow_json = json!({
"id": format!("swift_mt_{}_workflow", message_type.to_lowercase()),
"name": format!("SWIFT {} Processing Pipeline", message_type),
"description": format!("End-to-end processing pipeline for {} messages", message_type),
"priority": 0,
"condition": {
"==": [
{"var": "data.message_type"},
message_type
]
},
"tasks": [
{
"id": "step_1_generate",
"name": "Generate Sample JSON",
"description": "Generate sample JSON data from schema using datafake",
"function": {
"name": "generate_mt",
"input": {
"target": "sample_json"
}
},
},
{
"id": "step_2_publish",
"name": "Publish to MT Format",
"description": "Convert sample JSON data to SWIFT MT message format",
"function": {
"name": "publish_mt",
"input": {
"source": "sample_json",
"target": "sample_mt"
}
},
},
{
"id": "step_3_validate",
"name": "Validate MT Message",
"description": "Validate MT message against SWIFT standards",
"function": {
"name": "validate_mt",
"input": {
"source": "sample_mt",
"target": "validation_result"
}
},
},
{
"id": "step_4_parse",
"name": "Parse MT Message",
"description": "Parse MT message back to structured JSON format",
"function": {
"name": "parse_mt",
"input": {
"source": "sample_mt",
"target": "mt_json"
}
},
}
],
});
let workflow_str =
serde_json::to_string(&workflow_json).expect("Failed to serialize workflow JSON");
Workflow::from_json(&workflow_str).expect("Failed to parse workflow JSON")
}
fn normalize_json_value(value: &Value) -> Value {
match value {
Value::Number(n) => {
if let Some(f) = n.as_f64() {
let rounded = (f * 100.0).round() / 100.0;
if rounded.fract() == 0.0 && rounded.is_finite() {
Value::Number(serde_json::Number::from(rounded as i64))
} else {
serde_json::Number::from_f64(rounded)
.map(Value::Number)
.unwrap_or_else(|| value.clone())
}
} else {
value.clone()
}
}
Value::Object(obj) => {
let mut normalized = serde_json::Map::new();
for (k, v) in obj {
if !v.is_null() {
normalized.insert(k.clone(), normalize_json_value(v));
}
}
Value::Object(normalized)
}
Value::Array(arr) => Value::Array(arr.iter().map(normalize_json_value).collect()),
_ => value.clone(),
}
}
fn check_round_trip_success(message: &Message) -> bool {
let sample_json = message.data().get("sample_json");
let mt_json = message.data().get("mt_json");
match (sample_json, mt_json) {
(Some(original), Some(parsed)) => {
let original_data = original.get("json_data").unwrap_or(original);
let normalized_orig = normalize_json_value(original_data);
let normalized_parsed = normalize_json_value(parsed);
let orig_str = serde_json::to_string(&normalized_orig).unwrap_or_default();
let parsed_str = serde_json::to_string(&normalized_parsed).unwrap_or_default();
if std::env::var("TEST_DEBUG").unwrap_or_default() == "1" && orig_str != parsed_str {
println!("\n Debug round-trip comparison:");
println!(" JSON strings are different");
if let (Some(orig_obj), Some(parsed_obj)) =
(normalized_orig.as_object(), normalized_parsed.as_object())
{
println!(
" Original keys: {:?}",
orig_obj.keys().collect::<Vec<_>>()
);
println!(
" Parsed keys: {:?}",
parsed_obj.keys().collect::<Vec<_>>()
);
for key in orig_obj.keys() {
let orig_val =
serde_json::to_string(orig_obj.get(key).unwrap()).unwrap_or_default();
let parsed_val =
serde_json::to_string(parsed_obj.get(key).unwrap_or(&Value::Null))
.unwrap_or_default();
if orig_val != parsed_val {
println!(" Mismatch in '{}' field!", key);
if key == "fields" {
if let (Some(orig_fields), Some(parsed_fields)) = (
orig_obj.get("fields").and_then(|f| f.as_object()),
parsed_obj.get("fields").and_then(|f| f.as_object()),
) {
let mut all_keys: std::collections::HashSet<_> =
orig_fields.keys().collect();
all_keys.extend(parsed_fields.keys());
for field_key in all_keys {
let orig_field = orig_fields.get(field_key);
let parsed_field = parsed_fields.get(field_key);
match (orig_field, parsed_field) {
(Some(o), Some(p)) => {
let orig_str =
serde_json::to_string(o).unwrap_or_default();
let parsed_str =
serde_json::to_string(p).unwrap_or_default();
if orig_str != parsed_str {
println!(" Field {} differs:", field_key);
println!(" Original: {}", orig_str);
println!(" Parsed: {}", parsed_str);
}
}
(Some(o), None) => {
println!(
" Field {} only in original:",
field_key
);
println!(
" Original: {}",
serde_json::to_string(o).unwrap_or_default()
);
}
(None, Some(p)) => {
println!(
" Field {} only in parsed:",
field_key
);
println!(
" Parsed: {}",
serde_json::to_string(p).unwrap_or_default()
);
}
(None, None) => {} }
}
}
} else {
println!(" Original: {}", orig_val);
println!(" Parsed: {}", parsed_val);
}
}
}
}
}
orig_str == parsed_str
}
_ => false,
}
}
fn analyze_workflow_results(message: &Message, debug_mode: bool) -> WorkflowResult {
let mut result = WorkflowResult {
generate_success: message.data().get("sample_json").is_some(),
publish_success: message.data().get("sample_mt").is_some(),
..Default::default()
};
if let Some(validation) = message.data().get("validation_result") {
result.validate_success = validation
.get("valid")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if let Some(errors) = validation.get("errors").and_then(|e| e.as_array()) {
result.validation_errors = errors
.iter()
.filter_map(|e| e.as_str().map(|s| s.to_string()))
.collect();
}
} else if debug_mode {
println!("\n WARNING: No validation_result found in message data");
}
result.parse_success = message.data().get("mt_json").is_some();
result.round_trip_match = check_round_trip_success(message);
result.transform_success = result.parse_success;
if debug_mode && (!result.is_fully_successful()) {
println!("\nWorkflow Step Results:");
println!(" 1. Generate: {}", status_symbol(result.generate_success));
println!(" 2. Publish: {}", status_symbol(result.publish_success));
println!(" 3. Parse: {}", status_symbol(result.parse_success));
println!(" 4. Validate: {}", status_symbol(result.validate_success));
println!(
" 5. Transform: {}",
status_symbol(result.transform_success)
);
println!(
" Round-trip match: {}",
status_symbol(result.round_trip_match)
);
if !result.validation_errors.is_empty() {
println!("\n Validation errors:");
for error in &result.validation_errors {
println!(" - {}", error);
}
}
if !result.round_trip_match {
println!("\n Round-trip comparison failed - sample_json vs mt_json differ");
}
}
result
}
#[derive(Debug, Default)]
struct WorkflowResult {
generate_success: bool,
publish_success: bool,
parse_success: bool,
validate_success: bool,
transform_success: bool,
round_trip_match: bool,
validation_errors: Vec<String>,
}
impl WorkflowResult {
fn is_fully_successful(&self) -> bool {
self.generate_success
&& self.publish_success
&& self.parse_success
&& self.validate_success
&& self.transform_success
&& self.round_trip_match
}
}
#[derive(Debug)]
struct TestResult {
message_type: String,
scenario: String,
workflow_completed: bool,
generate_success: bool,
publish_success: bool,
parse_success: bool,
validation_passed: bool,
transform_success: bool,
round_trip_success: bool,
error: Option<String>,
}
impl TestResult {
fn is_fully_successful(&self) -> bool {
self.workflow_completed
&& self.generate_success
&& self.publish_success
&& self.parse_success
&& self.validation_passed
&& self.transform_success
&& self.round_trip_success
}
}
fn print_test_summary(results: &[TestResult]) {
let mut scenario_results: HashMap<String, Vec<&TestResult>> = HashMap::new();
for result in results {
let key = format!("{}/{}", result.message_type, result.scenario);
scenario_results.entry(key).or_default().push(result);
}
let mut sorted_scenarios: Vec<_> = scenario_results.iter().collect();
sorted_scenarios.sort_by_key(|(key, _)| key.as_str());
println!(
"\n╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗"
);
println!(
"║ SWIFT MT Workflow Pipeline Test Results ║"
);
println!(
"╠═════════╤══════════════════════════════════════════════════════════╤════════╤══════════════╤═════════════╤═════════════╤══════════════╤═════════════╤══════════════╣"
);
println!(
"║ Message │ Scenario │Samples │ Generate │ Publish │ Parse │ Validate │ Transform │ Round-trip ║"
);
println!(
"╟─────────┼──────────────────────────────────────────────────────────┼────────┼──────────────┼─────────────┼─────────────┼──────────────┼─────────────┼──────────────╢"
);
for (scenario_key, scenario_tests) in sorted_scenarios {
let parts: Vec<&str> = scenario_key.split('/').collect();
let message_type = parts.first().unwrap_or(&"");
let scenario_name = parts.get(1).unwrap_or(&"").trim_end_matches(".json");
let total = scenario_tests.len();
let generate_pass = scenario_tests.iter().filter(|r| r.generate_success).count();
let publish_pass = scenario_tests.iter().filter(|r| r.publish_success).count();
let parse_pass = scenario_tests.iter().filter(|r| r.parse_success).count();
let validation_pass = scenario_tests
.iter()
.filter(|r| r.validation_passed)
.count();
let transform_pass = scenario_tests
.iter()
.filter(|r| r.transform_success)
.count();
let roundtrip_pass = scenario_tests
.iter()
.filter(|r| r.round_trip_success)
.count();
let generate_str = format!(
"{:>3}/{:<3} {}",
generate_pass,
total,
pass_fail_symbol(generate_pass, total)
);
let publish_str = format!(
"{:>3}/{:<3} {}",
publish_pass,
total,
pass_fail_symbol(publish_pass, total)
);
let parse_str = format!(
"{:>3}/{:<3} {}",
parse_pass,
total,
pass_fail_symbol(parse_pass, total)
);
let validate_str = format!(
"{:>3}/{:<3} {}",
validation_pass,
total,
pass_fail_symbol(validation_pass, total)
);
let transform_str = format!(
"{:>3}/{:<3} {}",
transform_pass,
total,
pass_fail_symbol(transform_pass, total)
);
let roundtrip_str = format!(
"{:>3}/{:<3} {:>2}",
roundtrip_pass,
total,
pass_fail_symbol(roundtrip_pass, total)
);
println!(
"║ {:^7} │ {:<56} │{:^9}│ {:^10} │ {:^10} │ {:^10} │ {:^11} │ {:^10} │ {:^11} ║",
message_type,
scenario_name,
total,
generate_str,
publish_str,
parse_str,
validate_str,
transform_str,
roundtrip_str
);
}
println!(
"╚═════════╧══════════════════════════════════════════════════════════╧═════════╧═════════════╧═════════════╧═════════════╧══════════════╧═════════════╧══════════════╝"
);
let total = results.len();
let _workflow_success = results.iter().filter(|r| r.workflow_completed).count();
let generate_success = results.iter().filter(|r| r.generate_success).count();
let publish_success = results.iter().filter(|r| r.publish_success).count();
let parse_success = results.iter().filter(|r| r.parse_success).count();
let validation_success = results.iter().filter(|r| r.validation_passed).count();
let transform_success = results.iter().filter(|r| r.transform_success).count();
let roundtrip_success = results.iter().filter(|r| r.round_trip_success).count();
let fully_successful = results.iter().filter(|r| r.is_fully_successful()).count();
println!("\n📊 Summary:");
println!(" Total test samples: {}", total);
println!(
" Fully successful: {} ({}%)",
fully_successful,
percentage(fully_successful, total)
);
println!("\n Step Success Rates:");
println!(
" 1. Generate: {} ({}%)",
generate_success,
percentage(generate_success, total)
);
println!(
" 2. Publish: {} ({}%)",
publish_success,
percentage(publish_success, total)
);
println!(
" 3. Parse: {} ({}%)",
parse_success,
percentage(parse_success, total)
);
println!(
" 4. Validate: {} ({}%)",
validation_success,
percentage(validation_success, total)
);
println!(
" 5. Transform: {} ({}%)",
transform_success,
percentage(transform_success, total)
);
println!(
" Round-trip match: {} ({}%)",
roundtrip_success,
percentage(roundtrip_success, total)
);
}
fn pass_fail_symbol(pass_count: usize, total_count: usize) -> &'static str {
if total_count == 0 {
"⏭️"
} else if pass_count == total_count {
"✅"
} else if pass_count == 0 {
"❌"
} else {
" ⚠️ "
}
}
fn status_symbol(success: bool) -> &'static str {
if success { "✅" } else { "❌" }
}
fn percentage(value: usize, total: usize) -> usize {
if total == 0 { 0 } else { (value * 100) / total }
}