Skip to content

Commit 7009f65

Browse files
committed
run sql test files
1 parent 2d307fa commit 7009f65

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed

tests/common/mod.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
use std::time::Duration;
22

33
use actix_web::{
4+
dev::{fn_service, ServiceRequest},
5+
http::header,
46
http::header::ContentType,
57
test::{self, TestRequest},
8+
web,
69
web::Data,
10+
App, HttpResponse, HttpServer,
711
};
812
use sqlpage::{
913
app_config::{test_database_url, AppConfig},
1014
webserver::http::{form_config, main_handler, payload_config},
1115
AppState,
1216
};
17+
use tokio::sync::oneshot;
18+
use tokio::task::JoinHandle;
1319

1420
pub async fn get_request_to_with_data(
1521
path: &str,
@@ -100,3 +106,54 @@ pub fn init_log() {
100106
.is_test(true)
101107
.try_init();
102108
}
109+
110+
fn format_request_line_and_headers(req: &ServiceRequest) -> String {
111+
let mut out = format!("{} {}", req.method(), req.uri());
112+
let mut headers: Vec<_> = req.headers().iter().collect();
113+
headers.sort_by_key(|(k, _)| k.as_str());
114+
for (k, v) in headers {
115+
if k.as_str().eq_ignore_ascii_case("date") {
116+
continue;
117+
}
118+
out.push_str(&format!("|{k}: {}", v.to_str().unwrap_or("?")));
119+
}
120+
out
121+
}
122+
123+
async fn format_body(req: &mut ServiceRequest) -> Vec<u8> {
124+
req.extract::<web::Bytes>()
125+
.await
126+
.map(|b| b.to_vec())
127+
.unwrap_or_default()
128+
}
129+
130+
fn build_echo_response(body: Vec<u8>, meta: String) -> HttpResponse {
131+
let mut resp = meta.into_bytes();
132+
resp.push(b'|');
133+
resp.extend_from_slice(&body);
134+
HttpResponse::Ok()
135+
.insert_header((header::DATE, "Mon, 24 Feb 2025 12:00:00 GMT"))
136+
.insert_header((header::CONTENT_TYPE, "text/plain"))
137+
.body(resp)
138+
}
139+
140+
pub fn start_echo_server(shutdown: oneshot::Receiver<()>) -> JoinHandle<()> {
141+
let server = HttpServer::new(|| {
142+
App::new().default_service(fn_service(|mut req: ServiceRequest| async move {
143+
let meta = format_request_line_and_headers(&req);
144+
let body = format_body(&mut req).await;
145+
let resp = build_echo_response(body, meta);
146+
Ok(req.into_response(resp))
147+
}))
148+
})
149+
.bind("localhost:62802")
150+
.unwrap()
151+
.shutdown_timeout(1)
152+
.run();
153+
tokio::spawn(async move {
154+
tokio::select! {
155+
_ = server => {},
156+
_ = shutdown => {},
157+
}
158+
})
159+
}

tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ mod core;
55
mod data_formats;
66
mod errors;
77
mod requests;
8+
pub mod sql_test_files;
89
mod transactions;
910
mod uploads;

tests/sql_test_files/mod.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use actix_web::test;
2+
use sqlpage::AppState;
3+
use std::time::Duration;
4+
use tokio::sync::oneshot;
5+
use tokio::task::JoinHandle;
6+
7+
#[actix_web::test]
8+
async fn run_all_sql_test_files() {
9+
let app_data = crate::common::make_app_data().await;
10+
let test_files = get_sql_test_files();
11+
12+
// Create a shutdown channel for the echo server
13+
let (shutdown_tx, shutdown_rx) = oneshot::channel();
14+
// Start echo server once for all tests
15+
let echo_handle = crate::common::start_echo_server(shutdown_rx);
16+
17+
// Wait for echo server to be ready
18+
wait_for_echo_server().await;
19+
20+
for test_file in test_files {
21+
let test_result = run_sql_test(&test_file, &app_data, &echo_handle).await;
22+
assert_test_result(test_result, &test_file);
23+
}
24+
25+
// Signal the echo server to shut down
26+
let _ = shutdown_tx.send(());
27+
// Wait for echo server to complete after all tests with a timeout
28+
match tokio::time::timeout(Duration::from_secs(2), echo_handle).await {
29+
Ok(_) => (),
30+
Err(_) => panic!("Echo server did not shut down within 2 seconds"),
31+
}
32+
}
33+
34+
async fn wait_for_echo_server() {
35+
let client = awc::Client::default();
36+
let start = std::time::Instant::now();
37+
let timeout = Duration::from_secs(5);
38+
39+
while start.elapsed() < timeout {
40+
match client.get("http://localhost:62802/").send().await {
41+
Ok(_) => return,
42+
Err(_) => {
43+
tokio::time::sleep(Duration::from_millis(100)).await;
44+
continue;
45+
}
46+
}
47+
}
48+
panic!("Echo server did not become ready within 5 seconds");
49+
}
50+
51+
fn get_sql_test_files() -> Vec<std::path::PathBuf> {
52+
let path = std::path::Path::new("tests/sql_test_files");
53+
std::fs::read_dir(path)
54+
.unwrap()
55+
.filter_map(|entry| {
56+
let entry = entry.ok()?;
57+
let path = entry.path();
58+
if path.extension()? == "sql" {
59+
Some(path)
60+
} else {
61+
None
62+
}
63+
})
64+
.collect()
65+
}
66+
67+
async fn run_sql_test(
68+
test_file: &std::path::Path,
69+
app_data: &actix_web::web::Data<AppState>,
70+
_echo_handle: &JoinHandle<()>,
71+
) -> anyhow::Result<String> {
72+
let test_file_path = test_file.to_string_lossy().replace('\\', "/");
73+
let req_str = format!("/{test_file_path}?x=1");
74+
75+
let resp = tokio::time::timeout(
76+
Duration::from_secs(5),
77+
crate::common::req_path_with_app_data(&req_str, app_data.clone()),
78+
)
79+
.await
80+
.map_err(|e| anyhow::anyhow!("Test request timed out after 5 seconds: {}", e))??;
81+
82+
let body = test::read_body(resp).await;
83+
Ok(String::from_utf8(body.to_vec())?)
84+
}
85+
86+
fn assert_test_result(result: anyhow::Result<String>, test_file: &std::path::Path) {
87+
let (body, stem) = get_test_body_and_stem(result, test_file);
88+
assert_html_response(&body, test_file);
89+
let lowercase_body = body.to_lowercase();
90+
91+
if stem.starts_with("it_works") {
92+
assert_it_works_tests(&body, &lowercase_body, test_file);
93+
} else if stem.starts_with("error_") {
94+
assert_error_tests(&stem, &lowercase_body, test_file);
95+
}
96+
}
97+
98+
fn get_test_body_and_stem(
99+
result: anyhow::Result<String>,
100+
test_file: &std::path::Path,
101+
) -> (String, String) {
102+
let stem = test_file.file_stem().unwrap().to_str().unwrap().to_string();
103+
let body = result
104+
.unwrap_or_else(|e| panic!("Failed to get response for {}: {}", test_file.display(), e));
105+
(body, stem)
106+
}
107+
108+
fn assert_html_response(body: &str, test_file: &std::path::Path) {
109+
assert!(
110+
body.starts_with("<!DOCTYPE html>"),
111+
"Response to {} is not HTML",
112+
test_file.display()
113+
);
114+
}
115+
116+
fn assert_it_works_tests(body: &str, lowercase_body: &str, test_file: &std::path::Path) {
117+
assert!(
118+
body.contains("It works !"),
119+
"{}\n{}\nexpected to contain: It works !",
120+
test_file.display(),
121+
body
122+
);
123+
assert!(
124+
!lowercase_body.contains("error"),
125+
"{}\n{}\nexpected to not contain: error",
126+
test_file.display(),
127+
body
128+
);
129+
}
130+
131+
fn assert_error_tests(stem: &str, lowercase_body: &str, test_file: &std::path::Path) {
132+
let expected_error = stem
133+
.strip_prefix("error_")
134+
.unwrap()
135+
.replace('_', " ")
136+
.to_lowercase();
137+
assert!(
138+
lowercase_body.contains(&expected_error),
139+
"{}\n{}\nexpected to contain: {}",
140+
test_file.display(),
141+
lowercase_body,
142+
expected_error
143+
);
144+
assert!(
145+
lowercase_body.contains("error"),
146+
"{}\n{}\nexpected to contain: error",
147+
test_file.display(),
148+
lowercase_body
149+
);
150+
}

0 commit comments

Comments
 (0)