Skip to content

Commit 1a84903

Browse files
committed
First pass at running conformance tests
Signed-off-by: Ryan Levick <[email protected]>
1 parent 13a133f commit 1a84903

File tree

4 files changed

+187
-1
lines changed

4 files changed

+187
-1
lines changed

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ llm-metal = ["llm", "spin-trigger-http/llm-metal"]
117117
llm-cublas = ["llm", "spin-trigger-http/llm-cublas"]
118118

119119
[workspace]
120-
members = ["crates/*", "tests/runtime-tests", "tests/testing-framework"]
120+
members = ["crates/*", "tests/conformance-tests", "tests/runtime-tests", "tests/testing-framework"]
121121

122122
[workspace.dependencies]
123123
anyhow = "1.0.75"

tests/conformance-tests/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "conformance-tests"
3+
version.workspace = true
4+
authors.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
homepage.workspace = true
8+
repository.workspace = true
9+
rust-version.workspace = true
10+
11+
[dependencies]
12+
anyhow = "1.0"
13+
json5 = "0.4.1"
14+
serde = "1.0"
15+
testing-framework = { path = "../testing-framework" }
16+
17+
[lints]
18+
workspace = true

tests/conformance-tests/src/main.rs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
use std::collections::HashMap;
2+
3+
use anyhow::Context as _;
4+
5+
fn main() {
6+
let dir = std::fs::read_dir("/Users/rylev/Code/fermyon/conformance-test/tests").unwrap();
7+
for entry in dir {
8+
let entry = entry.unwrap();
9+
let spin_binary = "/Users/rylev/.local/bin/spin".into();
10+
let test_config = std::fs::read_to_string(entry.path().join("test.json5")).unwrap();
11+
let test_config: TestConfig = json5::from_str(&test_config).unwrap();
12+
let config = testing_framework::TestEnvironmentConfig::spin(
13+
spin_binary,
14+
["-f".into(), entry.path().to_str().unwrap().into()],
15+
move |e| {
16+
e.copy_into(entry.path().join("spin.toml"), "spin.toml")
17+
.context("failed to copy spin.toml")?;
18+
let mut cmd = std::process::Command::new("spin");
19+
cmd.env("CARGO_TARGET_DIR", entry.path().join("target"));
20+
cmd.args(["build", "-f", entry.path().to_str().unwrap()]);
21+
e.run_in(&mut cmd)?;
22+
Ok(())
23+
},
24+
testing_framework::ServicesConfig::none(),
25+
testing_framework::runtimes::SpinAppType::Http,
26+
);
27+
let mut env = testing_framework::TestEnvironment::up(config, |_| Ok(())).unwrap();
28+
let spin = env.runtime_mut();
29+
for invocation in test_config.invocations {
30+
let headers = invocation
31+
.request
32+
.headers
33+
.iter()
34+
.map(|h| (h.name.as_str(), h.value.as_str()))
35+
.collect::<Vec<_>>();
36+
let request = testing_framework::http::Request::full(
37+
invocation.request.method.into(),
38+
&invocation.request.path,
39+
&headers,
40+
invocation.request.body,
41+
);
42+
let response = spin.make_http_request(request).unwrap();
43+
let stderr = spin.stderr();
44+
let body = String::from_utf8(response.body())
45+
.unwrap_or_else(|_| String::from("invalid utf-8"));
46+
assert_eq!(
47+
response.status(),
48+
invocation.response.status,
49+
"request to Spin failed\nstderr:\n{stderr}\nbody:\n{body}",
50+
);
51+
52+
let mut actual_headers = response
53+
.headers()
54+
.iter()
55+
.map(|(k, v)| (k.to_lowercase(), v.to_lowercase()))
56+
.collect::<HashMap<_, _>>();
57+
for expected_header in invocation.response.headers {
58+
let expected_name = expected_header.name.to_lowercase();
59+
let expected_value = expected_header.value.map(|v| v.to_lowercase());
60+
let actual_value = actual_headers.remove(&expected_name);
61+
let Some(actual_value) = actual_value.as_deref() else {
62+
if expected_header.optional {
63+
continue;
64+
} else {
65+
panic!(
66+
"expected header {name} not found in response",
67+
name = expected_header.name
68+
)
69+
}
70+
};
71+
if let Some(expected_value) = expected_value {
72+
assert_eq!(actual_value, expected_value);
73+
}
74+
}
75+
if !actual_headers.is_empty() {
76+
panic!("unexpected headers: {actual_headers:?}");
77+
}
78+
79+
if let Some(expected_body) = invocation.response.body {
80+
assert_eq!(body, expected_body);
81+
}
82+
}
83+
}
84+
}
85+
86+
#[derive(Debug, serde::Deserialize)]
87+
struct TestConfig {
88+
invocations: Vec<Invocation>,
89+
}
90+
91+
#[derive(Debug, serde::Deserialize)]
92+
struct Invocation {
93+
request: Request,
94+
response: Response,
95+
}
96+
97+
#[derive(Debug, serde::Deserialize)]
98+
struct Request {
99+
#[serde(default)]
100+
method: Method,
101+
path: String,
102+
#[serde(default)]
103+
headers: Vec<RequestHeader>,
104+
#[serde(default)]
105+
body: Option<String>,
106+
}
107+
108+
#[derive(Debug, serde::Deserialize)]
109+
struct Response {
110+
#[serde(default = "default_status")]
111+
status: u16,
112+
headers: Vec<ResponseHeader>,
113+
body: Option<String>,
114+
}
115+
#[derive(Debug, serde::Deserialize)]
116+
struct RequestHeader {
117+
name: String,
118+
value: String,
119+
}
120+
121+
#[derive(Debug, serde::Deserialize)]
122+
struct ResponseHeader {
123+
name: String,
124+
value: Option<String>,
125+
#[serde(default)]
126+
optional: bool,
127+
}
128+
129+
#[derive(Debug, serde::Deserialize, Default)]
130+
enum Method {
131+
#[default]
132+
GET,
133+
POST,
134+
}
135+
136+
impl From<Method> for testing_framework::http::Method {
137+
fn from(method: Method) -> Self {
138+
match method {
139+
Method::GET => testing_framework::http::Method::GET,
140+
Method::POST => testing_framework::http::Method::POST,
141+
}
142+
}
143+
}
144+
145+
fn default_status() -> u16 {
146+
200
147+
}

0 commit comments

Comments
 (0)