Files
2025-10-02 10:51:11 +02:00

186 lines
5.7 KiB
Rust

use actix_web::test;
use sqlpage::AppState;
use std::time::Duration;
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
#[actix_web::test]
async fn run_all_sql_test_files() {
let app_data = crate::common::make_app_data().await;
let test_files = get_sql_test_files();
// Create a shutdown channel for the echo server
let (shutdown_tx, shutdown_rx) = oneshot::channel();
// Start echo server once for all tests
let (echo_handle, port) = crate::common::start_echo_server(shutdown_rx);
// Wait for echo server to be ready
wait_for_echo_server(port).await;
for test_file in test_files {
let test_result = run_sql_test(&test_file, &app_data, &echo_handle, port).await;
assert_test_result(test_result, &test_file);
}
// Signal the echo server to shut down
let _ = shutdown_tx.send(());
// Wait for echo server to complete after all tests with a timeout
match tokio::time::timeout(Duration::from_secs(2), echo_handle).await {
Ok(_) => (),
Err(_) => panic!("Echo server did not shut down within 2 seconds"),
}
}
async fn wait_for_echo_server(port: u16) {
let client = awc::Client::default();
let start = std::time::Instant::now();
let timeout = Duration::from_secs(5);
while start.elapsed() < timeout {
match client.get(format!("http://localhost:{port}/")).send().await {
Ok(_) => return,
Err(_) => {
tokio::time::sleep(Duration::from_millis(100)).await;
continue;
}
}
}
panic!("Echo server did not become ready within 5 seconds");
}
fn get_sql_test_files() -> Vec<std::path::PathBuf> {
let path = std::path::Path::new("tests/sql_test_files");
std::fs::read_dir(path)
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.extension()? == "sql" {
Some(path)
} else {
None
}
})
.collect()
}
use std::fmt::Write;
#[derive(Debug)]
enum TestResult {
Success(String),
Skipped(String),
}
async fn run_sql_test(
test_file: &std::path::Path,
app_data: &actix_web::web::Data<AppState>,
_echo_handle: &JoinHandle<()>,
port: u16,
) -> anyhow::Result<TestResult> {
let test_file_path = test_file.to_string_lossy().replace('\\', "/");
let stem = test_file.file_stem().unwrap().to_str().unwrap();
let app_state = app_data.get_ref();
let db = &app_state.db;
let db_type = format!("{:?}", db.info.database_type).to_lowercase();
if stem.contains(&format!("_no{db_type}")) {
return Ok(TestResult::Skipped(format!(
"Test skipped for database type: {db_type}"
)));
}
let mut query_params = "x=1".to_string();
if test_file_path.contains("fetch") {
write!(query_params, "&echo_port={port}").unwrap();
}
let req_str = format!("/{test_file_path}?{query_params}");
let resp = tokio::time::timeout(
Duration::from_secs(5),
crate::common::req_path_with_app_data(&req_str, app_data.clone()),
)
.await
.map_err(|e| anyhow::anyhow!("Test request timed out after 5 seconds: {}", e))??;
let body = test::read_body(resp).await;
Ok(TestResult::Success(String::from_utf8(body.to_vec())?))
}
fn assert_test_result(result: anyhow::Result<TestResult>, test_file: &std::path::Path) {
match result {
Ok(TestResult::Skipped(reason)) => {
println!("⏭️ Skipped {}: {}", test_file.display(), reason);
}
Ok(TestResult::Success(body)) => {
assert_html_response(&body, test_file);
let lowercase_body = body.to_lowercase();
let stem = test_file.file_stem().unwrap().to_str().unwrap().to_string();
if stem.starts_with("it_works") {
assert_it_works_tests(&body, &lowercase_body, test_file);
} else if stem.starts_with("error_") {
assert_error_tests(&stem, &lowercase_body, test_file);
}
}
Err(e) => panic!("Failed to get response for {}: {}", test_file.display(), e),
}
}
fn assert_html_response(body: &str, test_file: &std::path::Path) {
assert!(
body.starts_with("<!DOCTYPE html>"),
"Response to {} is not HTML",
test_file.display()
);
}
fn assert_it_works_tests(body: &str, lowercase_body: &str, test_file: &std::path::Path) {
if body.contains("<code class=\"sqlpage-error-description\">") {
let error_desc = body
.split("<code class=\"sqlpage-error-description\">")
.nth(1)
.and_then(|s| s.split("</code>").next())
.unwrap_or("Unknown error");
panic!(
"\n\n❌ TEST FAILED: {}\n\nFull Response:\n{}\n\nError Description: {}\n",
test_file.display(),
error_desc,
body
);
}
assert!(
body.contains("It works !"),
"{body}\n❌ Error in file {test_file:?} ❌\nShould contain: 'It works !'",
);
assert!(
!lowercase_body.contains("error"),
"{}\n{}\nexpected to not contain: error",
test_file.display(),
body
);
}
fn assert_error_tests(stem: &str, lowercase_body: &str, test_file: &std::path::Path) {
let expected_error = stem
.strip_prefix("error_")
.unwrap()
.replace('_', " ")
.to_lowercase();
assert!(
lowercase_body.contains(&expected_error),
"{}\n{}\nexpected to contain: {}",
test_file.display(),
lowercase_body,
expected_error
);
assert!(
lowercase_body.contains("error"),
"{}\n{}\nexpected to contain: error",
test_file.display(),
lowercase_body
);
}