post steel tests

This commit is contained in:
filipriec
2025-07-08 20:26:48 +02:00
parent be99cd9423
commit 2311fbaa3b
4 changed files with 974 additions and 5 deletions

View File

@@ -1,4 +1,4 @@
// tests/mod.rs
pub mod tables_data;
pub mod common;
pub mod table_definition;
// pub mod table_definition;

View File

@@ -1,6 +1,6 @@
// tests/tables_data/mod.rs
pub mod get;
pub mod delete;
// pub mod get;
// pub mod delete;
pub mod post;
pub mod put;
// pub mod put;

View File

@@ -1,3 +1,4 @@
// tests/tables_data/post/mod.rs
pub mod post_table_data_test;
// pub mod post_table_data_test;
pub mod post_table_data_steel_decimal_test;

View File

@@ -0,0 +1,968 @@
// tests/tables_data/post/post_table_data_steel_decimal_test.rs
use rstest::{fixture, rstest};
use sqlx::PgPool;
use sqlx::Row;
use std::collections::HashMap;
use prost_types::Value;
use prost_types::value::Kind;
use common::proto::multieko2::tables_data::PostTableDataRequest;
use common::proto::multieko2::table_definition::{
PostTableDefinitionRequest, ColumnDefinition as TableColumnDefinition
};
use server::tables_data::handlers::post_table_data;
use server::table_definition::handlers::post_table_definition;
use crate::common::setup_test_db;
use tonic::Status;
use tokio::sync::mpsc;
use server::indexer::IndexCommand;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::str::FromStr;
use rand::distr::Alphanumeric;
use rand::Rng;
// Helper function to generate unique identifiers for test isolation
fn generate_unique_id() -> String {
rand::rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect::<String>()
.to_lowercase()
}
// Helper to create a protobuf Value from a string
fn proto_string(s: &str) -> Value {
Value {
kind: Some(Kind::StringValue(s.to_string())),
}
}
// Helper to create a protobuf Value from a number
fn proto_number(n: f64) -> Value {
Value {
kind: Some(Kind::NumberValue(n)),
}
}
// Test context for Steel decimal integration tests
#[derive(Clone)]
struct SteelDecimalTestContext {
pool: PgPool,
profile_name: String,
invoice_table: String,
calculation_table: String,
validation_table: String,
}
// Create tables with Steel scripts that use decimal operations
async fn create_steel_decimal_test_tables(
pool: &PgPool,
profile_name: &str,
) -> Result<SteelDecimalTestContext, tonic::Status> {
let unique_id = generate_unique_id();
let invoice_table = format!("invoice_steel_{}", unique_id);
let calculation_table = format!("calc_steel_{}", unique_id);
let validation_table = format!("valid_steel_{}", unique_id);
// Table 1: Invoice table with price calculation script
let invoice_def = PostTableDefinitionRequest {
profile_name: profile_name.into(),
table_name: invoice_table.clone(),
columns: vec![
TableColumnDefinition { name: "item_name".into(), field_type: "text".into() },
TableColumnDefinition { name: "quantity".into(), field_type: "integer".into() },
TableColumnDefinition { name: "unit_price".into(), field_type: "decimal(10,2)".into() },
TableColumnDefinition { name: "total_price".into(), field_type: "decimal(10,2)".into() },
TableColumnDefinition { name: "tax_rate".into(), field_type: "decimal(5,4)".into() },
TableColumnDefinition { name: "final_amount".into(), field_type: "decimal(12,2)".into() },
],
indexes: vec![],
links: vec![],
};
post_table_definition(pool, invoice_def).await?;
// Table 2: Calculation table with complex decimal arithmetic
let calc_def = PostTableDefinitionRequest {
profile_name: profile_name.into(),
table_name: calculation_table.clone(),
columns: vec![
TableColumnDefinition { name: "operation_name".into(), field_type: "text".into() },
TableColumnDefinition { name: "input_a".into(), field_type: "decimal(15,5)".into() },
TableColumnDefinition { name: "input_b".into(), field_type: "decimal(15,5)".into() },
TableColumnDefinition { name: "result".into(), field_type: "decimal(20,5)".into() },
TableColumnDefinition { name: "precision_test".into(), field_type: "decimal(30,10)".into() },
],
indexes: vec![],
links: vec![],
};
post_table_definition(pool, calc_def).await?;
// Table 3: Validation table with business logic scripts
let validation_def = PostTableDefinitionRequest {
profile_name: profile_name.into(),
table_name: validation_table.clone(),
columns: vec![
TableColumnDefinition { name: "product_code".into(), field_type: "text".into() },
TableColumnDefinition { name: "price".into(), field_type: "decimal(10,2)".into() },
TableColumnDefinition { name: "discount_percent".into(), field_type: "decimal(5,2)".into() },
TableColumnDefinition { name: "discounted_price".into(), field_type: "decimal(10,2)".into() },
TableColumnDefinition { name: "margin_check".into(), field_type: "text".into() },
],
indexes: vec![],
links: vec![],
};
post_table_definition(pool, validation_def).await?;
// Insert Steel scripts that use decimal operations
// Script 1: Calculate total price (quantity * unit_price)
let total_price_script = r#"
(define (calculate-total-price quantity unit-price)
(steel/decimal/multiply
(steel/decimal/from-string quantity)
(steel/decimal/from-string unit-price)))
(steel/decimal/to-string
(calculate-total-price
(hash-get row-data "quantity")
(hash-get row-data "unit_price")))
"#;
// Script 2: Calculate final amount with tax (total_price * (1 + tax_rate))
let final_amount_script = r#"
(define (calculate-final-amount total-price tax-rate)
(let ([total-decimal (steel/decimal/from-string total-price)]
[tax-decimal (steel/decimal/from-string tax-rate)]
[one (steel/decimal/from-string "1")])
(steel/decimal/multiply
total-decimal
(steel/decimal/add one tax-decimal))))
(steel/decimal/to-string
(calculate-final-amount
(hash-get row-data "total_price")
(hash-get row-data "tax_rate")))
"#;
// Script 3: Complex arithmetic result (input_a + input_b) * sqrt(input_a)
let arithmetic_script = r#"
(define (complex-calculation a b)
(let ([a-decimal (steel/decimal/from-string a)]
[b-decimal (steel/decimal/from-string b)])
(let ([sum (steel/decimal/add a-decimal b-decimal)]
[sqrt-a (steel/decimal/sqrt a-decimal)])
(steel/decimal/multiply sum sqrt-a))))
(steel/decimal/to-string
(complex-calculation
(hash-get row-data "input_a")
(hash-get row-data "input_b")))
"#;
// Script 4: High precision calculation
let precision_script = r#"
(define (high-precision-calc a b)
(let ([a-decimal (steel/decimal/from-string a)]
[b-decimal (steel/decimal/from-string b)]
[pi (steel/decimal/from-string "3.1415926535")])
(steel/decimal/multiply
(steel/decimal/divide a-decimal b-decimal)
pi)))
(steel/decimal/to-string
(high-precision-calc
(hash-get row-data "input_a")
(hash-get row-data "input_b")))
"#;
// Script 5: Calculate discounted price (price * (1 - discount_percent/100))
let discount_script = r#"
(define (calculate-discount price discount-percent)
(let ([price-decimal (steel/decimal/from-string price)]
[discount-decimal (steel/decimal/from-string discount-percent)]
[hundred (steel/decimal/from-string "100")]
[one (steel/decimal/from-string "1")])
(let ([discount-ratio (steel/decimal/divide discount-decimal hundred)]
[multiplier (steel/decimal/subtract one discount-ratio)])
(steel/decimal/multiply price-decimal multiplier))))
(steel/decimal/to-string
(calculate-discount
(hash-get row-data "price")
(hash-get row-data "discount_percent")))
"#;
// Script 6: Margin validation check
let margin_check_script = r#"
(define (check-margin price discounted-price)
(let ([price-decimal (steel/decimal/from-string price)]
[discounted-decimal (steel/decimal/from-string discounted-price)]
[min-margin (steel/decimal/from-string "0.20")])
(let ([difference (steel/decimal/subtract price-decimal discounted-decimal)]
[margin-ratio (steel/decimal/divide difference price-decimal)])
(if (steel/decimal/greater-than? margin-ratio min-margin)
"ACCEPTABLE"
"TOO_LOW"))))
(check-margin
(hash-get row-data "price")
(hash-get row-data "discounted_price"))
"#;
// Get table definition IDs for script insertion
let invoice_table_id = sqlx::query_scalar!(
"SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2",
profile_name,
invoice_table
)
.fetch_one(pool)
.await
.map_err(|e| Status::internal(format!("Failed to get invoice table ID: {}", e)))?;
let calc_table_id = sqlx::query_scalar!(
"SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2",
profile_name,
calculation_table
)
.fetch_one(pool)
.await
.map_err(|e| Status::internal(format!("Failed to get calc table ID: {}", e)))?;
let validation_table_id = sqlx::query_scalar!(
"SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2",
profile_name,
validation_table
)
.fetch_one(pool)
.await
.map_err(|e| Status::internal(format!("Failed to get validation table ID: {}", e)))?;
// Get the schema_id for script insertion
let schema_id = sqlx::query_scalar!(
"SELECT id FROM schemas WHERE name = $1",
profile_name
)
.fetch_one(pool)
.await
.map_err(|e| Status::internal(format!("Failed to get schema ID: {}", e)))?;
// Insert scripts into table_scripts table with all required columns
let scripts = vec![
(invoice_table_id, &invoice_table, "total_price", "NUMERIC(10,2)", total_price_script, "Calculate total price (quantity * unit_price)"),
(invoice_table_id, &invoice_table, "final_amount", "NUMERIC(12,2)", final_amount_script, "Calculate final amount with tax"),
(calc_table_id, &calculation_table, "result", "NUMERIC(20,5)", arithmetic_script, "Complex arithmetic calculation"),
(calc_table_id, &calculation_table, "precision_test", "NUMERIC(30,10)", precision_script, "High precision calculation"),
(validation_table_id, &validation_table, "discounted_price", "NUMERIC(10,2)", discount_script, "Calculate discounted price"),
(validation_table_id, &validation_table, "margin_check", "TEXT", margin_check_script, "Validate margin requirements"),
];
for (table_id, target_table, target_column, target_column_type, script, description) in scripts {
sqlx::query!(
"INSERT INTO table_scripts (table_definitions_id, target_table, target_column, target_column_type, script, description, schema_id) VALUES ($1, $2, $3, $4, $5, $6, $7)",
table_id,
target_table,
target_column,
target_column_type,
script,
description,
schema_id
)
.execute(pool)
.await
.map_err(|e| Status::internal(format!("Failed to insert script: {}", e)))?;
}
Ok(SteelDecimalTestContext {
pool: pool.clone(),
profile_name: profile_name.to_string(),
invoice_table,
calculation_table,
validation_table,
})
}
async fn create_test_indexer_channel() -> mpsc::Sender<IndexCommand> {
let (tx, mut rx) = mpsc::channel(100);
// Spawn a task to consume indexer messages to prevent blocking
tokio::spawn(async move {
while let Some(_) = rx.recv().await {
// Just consume the messages
}
});
tx
}
#[fixture]
async fn steel_decimal_context() -> SteelDecimalTestContext {
let pool = setup_test_db().await;
let unique_id = generate_unique_id();
let profile_name = format!("steel_decimal_profile_{}", unique_id);
create_steel_decimal_test_tables(&pool, &profile_name).await
.expect("Failed to create steel decimal test tables")
}
// ========================================================================
// DEBUGGING TESTS TO UNDERSTAND STEEL DECIMAL INTEGRATION
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_steel_decimal_basic_function_registration(#[future] steel_decimal_context: SteelDecimalTestContext) {
let context = steel_decimal_context.await;
let indexer_tx = create_test_indexer_channel().await;
// Test the most basic steel-decimal function to see if it's registered
let debug_table = format!("basic_debug_{}", generate_unique_id());
let debug_def = PostTableDefinitionRequest {
profile_name: context.profile_name.clone(),
table_name: debug_table.clone(),
columns: vec![
TableColumnDefinition { name: "test_input".into(), field_type: "text".into() },
TableColumnDefinition { name: "result".into(), field_type: "text".into() },
],
indexes: vec![],
links: vec![],
};
post_table_definition(&context.pool, debug_def).await.unwrap();
// Get table and schema IDs
let debug_table_id = sqlx::query_scalar!(
"SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2",
context.profile_name,
debug_table
)
.fetch_one(&context.pool)
.await
.unwrap();
let schema_id = sqlx::query_scalar!(
"SELECT id FROM schemas WHERE name = $1",
context.profile_name
)
.fetch_one(&context.pool)
.await
.unwrap();
// Test the simplest possible steel-decimal function: decimal-add
let simple_script = r#"(decimal-add "1.0" "2.0")"#;
// Insert script
sqlx::query!(
"INSERT INTO table_scripts (table_definitions_id, target_table, target_column, target_column_type, script, description, schema_id) VALUES ($1, $2, $3, $4, $5, $6, $7)",
debug_table_id,
debug_table,
"result",
"TEXT",
simple_script,
"Test basic decimal-add function",
schema_id
)
.execute(&context.pool)
.await
.unwrap();
// Try to execute
let mut data = HashMap::new();
data.insert("test_input".into(), proto_string("test"));
data.insert("result".into(), proto_string("3.0")); // Expected result of 1.0 + 2.0
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: debug_table.clone(),
data,
};
let result = post_table_data(&context.pool, request, &indexer_tx).await;
match result {
Ok(response) => {
println!("✓ Basic steel-decimal function works!");
// Verify the result
let query = format!(
r#"SELECT result FROM "{}"."{}" WHERE id = $1"#,
context.profile_name, debug_table
);
let stored_result: String = sqlx::query_scalar(&query)
.bind(response.inserted_id)
.fetch_one(&context.pool)
.await
.unwrap();
assert_eq!(stored_result, "3.0", "Steel-decimal function should return correct result");
}
Err(e) => {
println!("✗ Basic steel-decimal function failed: {}", e.message());
panic!("Steel-decimal functions are not properly registered");
}
}
}
#[rstest]
#[tokio::test]
async fn test_debug_steel_decimal_function_availability(#[future] steel_decimal_context: SteelDecimalTestContext) {
let context = steel_decimal_context.await;
let indexer_tx = create_test_indexer_channel().await;
// Try different possible function name formats to see what works
let test_scripts = vec![
("decimal-add", r#"(decimal-add "1.0" "2.0")"#),
("decimal-mul", r#"(decimal-mul "2.0" "3.0")"#),
("decimal-sub", r#"(decimal-sub "5.0" "2.0")"#),
("decimal-div", r#"(decimal-div "10.0" "2.0")"#),
("decimal-gt", r#"(decimal-gt "5.0" "3.0")"#),
];
// Create a simple table for debugging
let debug_table = format!("debug_table_{}", generate_unique_id());
let debug_def = PostTableDefinitionRequest {
profile_name: context.profile_name.clone(),
table_name: debug_table.clone(),
columns: vec![
TableColumnDefinition { name: "test_name".into(), field_type: "text".into() },
TableColumnDefinition { name: "result".into(), field_type: "text".into() },
],
indexes: vec![],
links: vec![],
};
post_table_definition(&context.pool, debug_def).await.unwrap();
// Get table ID for script insertion
let debug_table_id = sqlx::query_scalar!(
"SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2",
context.profile_name,
debug_table
)
.fetch_one(&context.pool)
.await
.unwrap();
let schema_id = sqlx::query_scalar!(
"SELECT id FROM schemas WHERE name = $1",
context.profile_name
)
.fetch_one(&context.pool)
.await
.unwrap();
for (test_name, script) in test_scripts {
// Insert debug script
let insert_result = sqlx::query!(
"INSERT INTO table_scripts (table_definitions_id, target_table, target_column, target_column_type, script, description, schema_id) VALUES ($1, $2, $3, $4, $5, $6, $7)",
debug_table_id,
debug_table,
"result",
"TEXT",
script,
format!("Debug test for {}", test_name),
schema_id
)
.execute(&context.pool)
.await;
if insert_result.is_ok() {
// Try to execute the script
let mut data = HashMap::new();
data.insert("test_name".into(), proto_string(test_name));
data.insert("result".into(), proto_string("expected_value"));
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: debug_table.clone(),
data,
};
let result = post_table_data(&context.pool, request, &indexer_tx).await;
match result {
Ok(_) => println!("✓ Function {} works!", test_name),
Err(e) => println!("✗ Function {} failed: {}", test_name, e.message()),
}
// Clean up the script for next test
sqlx::query!(
"DELETE FROM table_scripts WHERE table_definitions_id = $1 AND target_column = $2",
debug_table_id,
"result"
)
.execute(&context.pool)
.await
.unwrap();
} else {
println!("✗ Failed to insert debug script for {}", test_name);
}
}
}
// ========================================================================
// BASIC STEEL DECIMAL FUNCTIONALITY TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_steel_decimal_basic_arithmetic(#[future] steel_decimal_context: SteelDecimalTestContext) {
let context = steel_decimal_context.await;
let indexer_tx = create_test_indexer_channel().await;
// Test basic multiplication: quantity * unit_price = total_price
let mut data = HashMap::new();
data.insert("item_name".into(), proto_string("Test Item"));
data.insert("quantity".into(), proto_number(5.0));
data.insert("unit_price".into(), proto_string("19.99"));
data.insert("total_price".into(), proto_string("99.95")); // 5 * 19.99 = 99.95
data.insert("tax_rate".into(), proto_string("0.1000")); // 10%
data.insert("final_amount".into(), proto_string("109.95")); // 99.95 * 1.1 = 109.945 -> 109.95
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.invoice_table.clone(),
data,
};
let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap();
assert!(response.success);
// Verify the calculated values were stored correctly
let query = format!(
r#"SELECT total_price, final_amount FROM "{}"."{}" WHERE id = $1"#,
context.profile_name, context.invoice_table
);
let row = sqlx::query(&query)
.bind(response.inserted_id)
.fetch_one(&context.pool)
.await
.unwrap();
let stored_total: Decimal = row.get("total_price");
let stored_final: Decimal = row.get("final_amount");
assert_eq!(stored_total, dec!(99.95));
assert_eq!(stored_final, dec!(109.95));
}
#[rstest]
#[tokio::test]
async fn test_steel_decimal_precision_preservation(#[future] steel_decimal_context: SteelDecimalTestContext) {
let context = steel_decimal_context.await;
let indexer_tx = create_test_indexer_channel().await;
// Test high precision calculations
let mut data = HashMap::new();
data.insert("operation_name".into(), proto_string("High Precision Test"));
data.insert("input_a".into(), proto_string("123.45678"));
data.insert("input_b".into(), proto_string("987.65432"));
// Steel script should calculate (a + b) * sqrt(a) and a/b * pi
data.insert("result".into(), proto_string("12345.67890")); // Placeholder, will be calculated
data.insert("precision_test".into(), proto_string("0.3926990125")); // Placeholder, will be calculated
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.calculation_table.clone(),
data,
};
let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap();
assert!(response.success);
// Verify precision is maintained in stored values
let query = format!(
r#"SELECT result, precision_test FROM "{}"."{}" WHERE id = $1"#,
context.profile_name, context.calculation_table
);
let row = sqlx::query(&query)
.bind(response.inserted_id)
.fetch_one(&context.pool)
.await
.unwrap();
let stored_result: Decimal = row.get("result");
let stored_precision: Decimal = row.get("precision_test");
// Verify that the results are calculated by Steel and have proper precision
assert!(stored_result.scale() <= 5); // Should fit in decimal(20,5)
assert!(stored_precision.scale() <= 10); // Should fit in decimal(30,10)
println!("Steel calculated result: {}", stored_result);
println!("Steel calculated precision test: {}", stored_precision);
}
#[rstest]
#[tokio::test]
async fn test_steel_decimal_business_logic_validation(#[future] steel_decimal_context: SteelDecimalTestContext) {
let context = steel_decimal_context.await;
let indexer_tx = create_test_indexer_channel().await;
// Test business logic with acceptable margin
let mut data = HashMap::new();
data.insert("product_code".into(), proto_string("PROD001"));
data.insert("price".into(), proto_string("100.00"));
data.insert("discount_percent".into(), proto_string("15.00")); // 15% discount
data.insert("discounted_price".into(), proto_string("85.00")); // 100 * (1 - 0.15) = 85
data.insert("margin_check".into(), proto_string("ACCEPTABLE")); // 15% margin > 20% minimum -> should be "TOO_LOW"
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.validation_table.clone(),
data,
};
let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap();
assert!(response.success);
// Verify business logic was applied correctly
let query = format!(
r#"SELECT discounted_price, margin_check FROM "{}"."{}" WHERE id = $1"#,
context.profile_name, context.validation_table
);
let row = sqlx::query(&query)
.bind(response.inserted_id)
.fetch_one(&context.pool)
.await
.unwrap();
let stored_discounted: Decimal = row.get("discounted_price");
let stored_margin_check: String = row.get("margin_check");
assert_eq!(stored_discounted, dec!(85.00));
assert_eq!(stored_margin_check, "TOO_LOW"); // 15% margin is less than 20% minimum
}
// ========================================================================
// STEEL DECIMAL ERROR HANDLING TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_steel_decimal_division_by_zero_handling(#[future] steel_decimal_context: SteelDecimalTestContext) {
let context = steel_decimal_context.await;
let indexer_tx = create_test_indexer_channel().await;
// Test division by zero in Steel script
let mut data = HashMap::new();
data.insert("operation_name".into(), proto_string("Division by Zero Test"));
data.insert("input_a".into(), proto_string("100.00"));
data.insert("input_b".into(), proto_string("0.00")); // Division by zero
data.insert("result".into(), proto_string("0.00"));
data.insert("precision_test".into(), proto_string("0.00"));
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.calculation_table.clone(),
data,
};
let result = post_table_data(&context.pool, request, &indexer_tx).await;
// Should fail due to division by zero in Steel script
assert!(result.is_err());
if let Err(err) = result {
assert_eq!(err.code(), tonic::Code::InvalidArgument);
assert!(err.message().contains("Script execution failed"));
}
}
#[rstest]
#[tokio::test]
async fn test_steel_decimal_invalid_input_format(#[future] steel_decimal_context: SteelDecimalTestContext) {
let context = steel_decimal_context.await;
let indexer_tx = create_test_indexer_channel().await;
// Test invalid decimal format in Steel script input
let mut data = HashMap::new();
data.insert("item_name".into(), proto_string("Invalid Input Test"));
data.insert("quantity".into(), proto_number(3.0));
data.insert("unit_price".into(), proto_string("not-a-number")); // Invalid decimal
data.insert("total_price".into(), proto_string("0.00"));
data.insert("tax_rate".into(), proto_string("0.1000"));
data.insert("final_amount".into(), proto_string("0.00"));
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.invoice_table.clone(),
data,
};
let result = post_table_data(&context.pool, request, &indexer_tx).await;
// Should fail due to invalid decimal format
assert!(result.is_err());
if let Err(err) = result {
assert_eq!(err.code(), tonic::Code::InvalidArgument);
assert!(err.message().contains("Script execution failed"));
}
}
// ========================================================================
// STEEL DECIMAL ROUNDTRIP AND CONVERSION TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_steel_decimal_roundtrip_conversion(#[future] steel_decimal_context: SteelDecimalTestContext) {
let context = steel_decimal_context.await;
let indexer_tx = create_test_indexer_channel().await;
// Test various decimal formats and precision levels
let test_cases = vec![
("0.01", 2.0, "Small decimal"),
("999.99", 10.0, "Large decimal with cents"),
("123.456789", 1.0, "High precision input"),
("0.00001", 100000.0, "Very small decimal"),
];
for (unit_price, quantity, description) in test_cases {
let mut data = HashMap::new();
data.insert("item_name".into(), proto_string(description));
data.insert("quantity".into(), proto_number(quantity));
data.insert("unit_price".into(), proto_string(unit_price));
data.insert("total_price".into(), proto_string("0.00")); // Will be calculated
data.insert("tax_rate".into(), proto_string("0.0500")); // 5%
data.insert("final_amount".into(), proto_string("0.00")); // Will be calculated
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.invoice_table.clone(),
data,
};
let response = post_table_data(&context.pool, request, &indexer_tx).await
.expect(&format!("Failed for test case: {}", description));
// Verify that Steel decimal calculations are consistent
let query = format!(
r#"SELECT unit_price, quantity, total_price, final_amount FROM "{}"."{}" WHERE id = $1"#,
context.profile_name, context.invoice_table
);
let row = sqlx::query(&query)
.bind(response.inserted_id)
.fetch_one(&context.pool)
.await
.unwrap();
let stored_unit_price: Decimal = row.get("unit_price");
let stored_quantity: i32 = row.get("quantity");
let stored_total: Decimal = row.get("total_price");
let stored_final: Decimal = row.get("final_amount");
// Verify Steel calculations match expected Rust calculations
let expected_unit_price = Decimal::from_str(unit_price).unwrap();
let expected_total = expected_unit_price * Decimal::from(stored_quantity);
let expected_final = expected_total * dec!(1.05); // 5% tax
assert_eq!(stored_unit_price, expected_unit_price);
assert_eq!(stored_total, expected_total);
assert_eq!(stored_final, expected_final);
println!("✓ Roundtrip test passed for {}: {} * {} = {} -> {}",
description, unit_price, quantity, stored_total, stored_final);
}
}
#[rstest]
#[tokio::test]
async fn test_steel_decimal_negative_values(#[future] steel_decimal_context: SteelDecimalTestContext) {
let context = steel_decimal_context.await;
let indexer_tx = create_test_indexer_channel().await;
// Test negative discount (surcharge)
let mut data = HashMap::new();
data.insert("product_code".into(), proto_string("SURCHG001"));
data.insert("price".into(), proto_string("50.00"));
data.insert("discount_percent".into(), proto_string("-10.00")); // 10% surcharge
data.insert("discounted_price".into(), proto_string("55.00")); // 50 * (1 - (-0.1)) = 50 * 1.1 = 55
data.insert("margin_check".into(), proto_string("ACCEPTABLE")); // Will be calculated
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.validation_table.clone(),
data,
};
let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap();
// Verify negative percentage handling
let query = format!(
r#"SELECT discounted_price, margin_check FROM "{}"."{}" WHERE id = $1"#,
context.profile_name, context.validation_table
);
let row = sqlx::query(&query)
.bind(response.inserted_id)
.fetch_one(&context.pool)
.await
.unwrap();
let stored_discounted: Decimal = row.get("discounted_price");
let stored_margin_check: String = row.get("margin_check");
assert_eq!(stored_discounted, dec!(55.00));
// With surcharge, margin should be acceptable (negative discount increases final price)
assert_eq!(stored_margin_check, "ACCEPTABLE");
}
// ========================================================================
// CONCURRENT STEEL DECIMAL EXECUTION TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_concurrent_steel_decimal_calculations(#[future] steel_decimal_context: SteelDecimalTestContext) {
let context = steel_decimal_context.await;
use futures::future::join_all;
// Test concurrent Steel decimal calculations
let tasks: Vec<_> = (0..10).map(|i| {
let context = context.clone();
async move {
let indexer_tx = create_test_indexer_channel().await;
let unit_price = format!("{}.99", 10 + i);
let quantity = i + 1;
let mut data = HashMap::new();
data.insert("item_name".into(), proto_string(&format!("Concurrent Item {}", i)));
data.insert("quantity".into(), proto_number(quantity as f64));
data.insert("unit_price".into(), proto_string(&unit_price));
data.insert("total_price".into(), proto_string("0.00"));
data.insert("tax_rate".into(), proto_string("0.2000")); // 20%
data.insert("final_amount".into(), proto_string("0.00"));
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.invoice_table.clone(),
data,
};
post_table_data(&context.pool, request, &indexer_tx).await
}
}).collect();
let results = join_all(tasks).await;
// All Steel calculations should succeed concurrently
for (i, result) in results.into_iter().enumerate() {
assert!(result.is_ok(), "Concurrent Steel calculation {} should succeed", i);
}
// Verify all calculations were performed correctly
let query = format!(
r#"SELECT COUNT(*) as count FROM "{}"."{}" WHERE item_name LIKE 'Concurrent Item%'"#,
context.profile_name, context.invoice_table
);
let count: i64 = sqlx::query_scalar(&query)
.fetch_one(&context.pool)
.await
.unwrap();
assert_eq!(count, 10);
}
// ========================================================================
// STEEL DECIMAL CRATE FEATURE VERIFICATION TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_steel_decimal_crate_features_available(#[future] steel_decimal_context: SteelDecimalTestContext) {
let context = steel_decimal_context.await;
let indexer_tx = create_test_indexer_channel().await;
// Test that Steel decimal crate functions are properly registered
// This tests sqrt, which should only be available if steel-decimal is properly integrated
let mut data = HashMap::new();
data.insert("operation_name".into(), proto_string("Steel Decimal Features Test"));
data.insert("input_a".into(), proto_string("25.00")); // sqrt(25) = 5
data.insert("input_b".into(), proto_string("4.00")); // sqrt(4) = 2
data.insert("result".into(), proto_string("0.00")); // Should be (25 + 4) * 5 = 145
data.insert("precision_test".into(), proto_string("0.00")); // Should be 25/4 * pi ≈ 19.6349
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.calculation_table.clone(),
data,
};
let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap();
// Verify that advanced steel-decimal functions (like sqrt) worked
let query = format!(
r#"SELECT result, precision_test FROM "{}"."{}" WHERE id = $1"#,
context.profile_name, context.calculation_table
);
let row = sqlx::query(&query)
.bind(response.inserted_id)
.fetch_one(&context.pool)
.await
.unwrap();
let stored_result: Decimal = row.get("result");
let stored_precision: Decimal = row.get("precision_test");
// If steel-decimal is properly integrated, sqrt function should work
// Result should be (25 + 4) * sqrt(25) = 29 * 5 = 145
assert_eq!(stored_result, dec!(145.00000));
// Precision test: 25/4 * π ≈ 19.634954...
assert!(stored_precision > dec!(19.6) && stored_precision < dec!(19.7));
println!("✓ Steel decimal crate features verified:");
println!(" - sqrt function available and working");
println!(" - High precision arithmetic: {}", stored_precision);
println!(" - Complex calculations: {}", stored_result);
}
#[rstest]
#[tokio::test]
async fn test_steel_decimal_vs_native_rust_consistency(#[future] steel_decimal_context: SteelDecimalTestContext) {
let context = steel_decimal_context.await;
let indexer_tx = create_test_indexer_channel().await;
// Test that Steel decimal calculations match native Rust decimal calculations
let test_cases = vec![
("100.00", "0.15", "Steel vs Rust consistency test 1"),
("999.99", "0.0825", "Steel vs Rust consistency test 2"),
("0.01", "0.9999", "Steel vs Rust edge case"),
];
for (price, discount_rate, description) in test_cases {
let mut data = HashMap::new();
data.insert("product_code".into(), proto_string("CONSISTENCY"));
data.insert("price".into(), proto_string(price));
data.insert("discount_percent".into(), proto_string(&format!("{}", Decimal::from_str(discount_rate).unwrap() * dec!(100))));
data.insert("discounted_price".into(), proto_string("0.00")); // Calculated by Steel
data.insert("margin_check".into(), proto_string("TEST")); // Calculated by Steel
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.validation_table.clone(),
data,
};
let response = post_table_data(&context.pool, request, &indexer_tx).await
.expect(&format!("Failed for consistency test: {}", description));
// Get Steel-calculated result
let query = format!(
r#"SELECT discounted_price FROM "{}"."{}" WHERE id = $1"#,
context.profile_name, context.validation_table
);
let steel_result: Decimal = sqlx::query_scalar(&query)
.bind(response.inserted_id)
.fetch_one(&context.pool)
.await
.unwrap();
// Calculate using native Rust decimal
let price_decimal = Decimal::from_str(price).unwrap();
let discount_decimal = Decimal::from_str(discount_rate).unwrap();
let rust_result = price_decimal * (dec!(1) - discount_decimal);
// Steel and Rust calculations should match exactly
assert_eq!(steel_result, rust_result,
"Steel vs Rust mismatch for {}: Steel={}, Rust={}",
description, steel_result, rust_result);
println!("✓ Consistency verified for {}: {}", description, steel_result);
}
}