Skip to content

Commit 82a3fda

Browse files
rylevkate-goldenring
authored andcommitted
Move to new test-environment crate
Signed-off-by: Ryan Levick <[email protected]>
1 parent 155c7e4 commit 82a3fda

File tree

3 files changed

+221
-63
lines changed

3 files changed

+221
-63
lines changed

conformance-tests/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ homepage.workspace = true
99

1010
[dependencies]
1111
anyhow = "1.0"
12-
testing-framework = { git = "https://github.com/kate-goldenring/spin", branch = "shim-test-environment" }
13-
conformance-tests = { git = "https://github.com/fermyon/conformance-tests" }
12+
conformance-tests = { git = "https://github.com/fermyon/conformance-tests", branch = "testing-environment" }
13+
log = "0.4"
14+
nix = "0.26"
15+
test-environment = { git = "https://github.com/fermyon/conformance-tests", branch = "testing-environment" }

conformance-tests/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Containerd must be configured to access the `containerd-shim-spin`:
1818
2. Configure containerd to add the Spin shim as a runtime by adding the following to `/etc/containerd/config.toml`:
1919
```toml
2020
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.spin]
21-
runtime_type = "io.containerd.spin.v2"
21+
runtime_type = "/usr/bin/containerd-shim-spin-v2"
2222
```
2323
3. Restart containerd if it is running as a service
2424
```sh

conformance-tests/src/main.rs

Lines changed: 216 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,244 @@
1-
use testing_framework::runtimes::spin_containerd_shim::SpinShim;
1+
use std::{
2+
path::{Path, PathBuf},
3+
process::{Command, Stdio},
4+
};
5+
6+
use anyhow::Context as _;
7+
use test_environment::{
8+
http::{Request, Response},
9+
io::OutputStream,
10+
services::ServicesConfig,
11+
Runtime, TestEnvironment, TestEnvironmentConfig,
12+
};
213

314
fn main() {
415
let tests_dir = conformance_tests::download_tests().unwrap();
16+
let mut args = std::env::args().skip(1);
17+
let spin_binary = args
18+
.next()
19+
.expect("expected first arg to be path to Spin binary");
20+
let ctr_binary = args
21+
.next()
22+
.expect("expected second arg to be path to ctr binary");
23+
let ctr_binary = Path::new(&ctr_binary);
524

625
for test in conformance_tests::tests(&tests_dir).unwrap() {
7-
let spin_binary = std::path::Path::new("/home/kagold/projects/temp/spin");
8-
let ctr_binary = "/usr/bin/ctr".into();
26+
let spin_binary = PathBuf::from(spin_binary.clone());
927
// Just using TTL.sh until we decide where to host these (local registry, ghcr, etc)
1028
let oci_image = format!("ttl.sh/{}:72h", test.name);
11-
let env_config = testing_framework::TestEnvironmentConfig::containerd_shim(
12-
ctr_binary,
29+
let env_config = SpinShim::config(
30+
ctr_binary.into(),
1331
oci_image.clone(),
1432
move |e| {
1533
// TODO: consider doing this outside the test environment since it is not a part of the runtime execution
1634
e.copy_into(&test.manifest, "spin.toml")?;
1735
e.copy_into(&test.component, test.component.file_name().unwrap())?;
18-
SpinShim::regisry_push(spin_binary, &oci_image, e).unwrap();
36+
SpinShim::regisry_push(&spin_binary, &oci_image, e).unwrap();
1937
Ok(())
2038
},
21-
testing_framework::ServicesConfig::none(),
39+
test_environment::services::ServicesConfig::none(),
2240
);
23-
std::thread::sleep(std::time::Duration::from_secs(60));
24-
let mut env = testing_framework::TestEnvironment::up(env_config, |_| Ok(())).unwrap();
41+
let mut env = TestEnvironment::up(env_config, |_| Ok(())).unwrap();
2542
let spin = env.runtime_mut();
2643
for invocation in test.config.invocations {
2744
let conformance_tests::config::Invocation::Http(invocation) = invocation;
28-
let headers = invocation
45+
invocation
2946
.request
30-
.headers
31-
.iter()
32-
.map(|h| (h.name.as_str(), h.value.as_str()))
33-
.collect::<Vec<_>>();
34-
let request = testing_framework::http::Request::full(
35-
match invocation.request.method {
36-
conformance_tests::config::Method::GET => testing_framework::http::Method::GET,
37-
conformance_tests::config::Method::POST => {
38-
testing_framework::http::Method::POST
39-
}
40-
},
41-
&invocation.request.path,
42-
&headers,
43-
invocation.request.body,
44-
);
45-
let response = spin.make_http_request(request).unwrap();
46-
let stderr = spin.stderr();
47-
let body = String::from_utf8(response.body())
48-
.unwrap_or_else(|_| String::from("invalid utf-8"));
49-
assert_eq!(
50-
response.status(),
51-
invocation.response.status,
52-
"request to Spin failed\nstderr:\n{stderr}\nbody:\n{body}",
53-
);
47+
.send(|request| spin.make_http_request(request))
48+
.unwrap();
49+
}
50+
println!("test passed: {}", test.name);
51+
}
52+
}
53+
54+
struct SpinShim {
55+
process: std::process::Child,
56+
#[allow(dead_code)]
57+
stdout: OutputStream,
58+
stderr: OutputStream,
59+
io_mode: IoMode,
60+
}
61+
62+
/// `ctr run` invocations require an ID that is unique to all currently running instances. Since
63+
/// only one test runs at a time, we can reuse a constant ID.
64+
const CTR_RUN_ID: &str = "run-id";
65+
66+
impl SpinShim {
67+
pub fn config(
68+
ctr_binary: PathBuf,
69+
oci_image: String,
70+
preboot: impl FnOnce(&mut TestEnvironment<SpinShim>) -> anyhow::Result<()> + 'static,
71+
services_config: ServicesConfig,
72+
) -> TestEnvironmentConfig<SpinShim> {
73+
TestEnvironmentConfig {
74+
services_config,
75+
create_runtime: Box::new(move |env| {
76+
preboot(env)?;
77+
SpinShim::image_pull(&ctr_binary, &oci_image)?;
78+
SpinShim::start(&ctr_binary, env, &oci_image, CTR_RUN_ID)
79+
}),
80+
}
81+
}
82+
83+
pub fn regisry_push<R>(
84+
spin_binary_path: &Path,
85+
image: &str,
86+
env: &mut TestEnvironment<R>,
87+
) -> anyhow::Result<()> {
88+
// TODO: consider enabling configuring a port
89+
Command::new(spin_binary_path)
90+
.args(["registry", "push"])
91+
.arg(image)
92+
.current_dir(env.path())
93+
.output()
94+
.context("failed to push spin app to registry with 'spin'")?;
95+
// TODO: assess output
96+
Ok(())
97+
}
98+
99+
pub fn image_pull(ctr_binary_path: &Path, image: &str) -> anyhow::Result<()> {
100+
// TODO: consider enabling configuring a port
101+
Command::new(ctr_binary_path)
102+
.args(["image", "pull"])
103+
.arg(image)
104+
.output()
105+
.context("failed to pull spin app with 'ctr'")?;
106+
// TODO: assess output
107+
Ok(())
108+
}
54109

55-
let mut actual_headers = response
56-
.headers()
57-
.iter()
58-
.map(|(k, v)| (k.to_lowercase(), v.to_lowercase()))
59-
.collect::<std::collections::HashMap<_, _>>();
60-
for expected_header in invocation.response.headers {
61-
let expected_name = expected_header.name.to_lowercase();
62-
let expected_value = expected_header.value.map(|v| v.to_lowercase());
63-
let actual_value = actual_headers.remove(&expected_name);
64-
let Some(actual_value) = actual_value.as_deref() else {
65-
if expected_header.optional {
66-
continue;
67-
} else {
68-
panic!(
69-
"expected header {name} not found in response",
70-
name = expected_header.name
71-
)
72-
}
73-
};
74-
if let Some(expected_value) = expected_value {
75-
assert_eq!(actual_value, expected_value);
110+
/// Start the Spin app using `ctr run`
111+
/// Equivalent of `sudo ctr run --rm --net-host --runtime io.containerd.spin.v2 ttl.sh/myapp:48h ctr-run-id bogus-arg` for image `ttl.sh/myapp:48h` and run id `ctr-run-id`
112+
pub fn start<R>(
113+
ctr_binary_path: &Path,
114+
env: &mut TestEnvironment<R>,
115+
image: &str,
116+
ctr_run_id: &str,
117+
) -> anyhow::Result<Self> {
118+
let port = 80;
119+
let mut ctr_cmd = std::process::Command::new(ctr_binary_path);
120+
let child = ctr_cmd
121+
.arg("run")
122+
.args([
123+
"--memory-limit",
124+
"10000",
125+
"--rm",
126+
"--net-host",
127+
"--runtime",
128+
"io.containerd.spin.v2",
129+
])
130+
.arg(image)
131+
.arg(ctr_run_id)
132+
.arg("bogus-arg")
133+
.stdout(Stdio::piped())
134+
.stderr(Stdio::piped());
135+
for (key, value) in env.env_vars() {
136+
child.env(key, value);
137+
}
138+
let mut child = child.spawn()?;
139+
let stdout = OutputStream::new(child.stdout.take().unwrap());
140+
let stderr = OutputStream::new(child.stderr.take().unwrap());
141+
log::debug!("Awaiting shim binary to start up on port {port}...");
142+
let mut spin = Self {
143+
process: child,
144+
stdout,
145+
stderr,
146+
io_mode: IoMode::Http(port),
147+
};
148+
let start = std::time::Instant::now();
149+
loop {
150+
match std::net::TcpStream::connect(format!("127.0.0.1:{port}")) {
151+
Ok(_) => {
152+
log::debug!("Spin shim started on port {port}.");
153+
return Ok(spin);
154+
}
155+
Err(e) => {
156+
let stderr = spin.stderr.output_as_str().unwrap_or("<non-utf8>");
157+
log::trace!("Checking that the Spin server started returned an error: {e}");
158+
log::trace!("Current spin stderr = '{stderr}'");
76159
}
77160
}
78-
if !actual_headers.is_empty() {
79-
panic!("unexpected headers: {actual_headers:?}");
161+
if let Some(status) = spin.try_wait()? {
162+
anyhow::bail!(
163+
"Shim exited early with status code {:?}\n{}{}",
164+
status.code(),
165+
spin.stdout.output_as_str().unwrap_or("<non-utf8>"),
166+
spin.stderr.output_as_str().unwrap_or("<non-utf8>")
167+
);
80168
}
81169

82-
if let Some(expected_body) = invocation.response.body {
83-
assert_eq!(body, expected_body);
170+
if start.elapsed() > std::time::Duration::from_secs(2 * 60) {
171+
break;
84172
}
173+
std::thread::sleep(std::time::Duration::from_millis(50));
85174
}
86-
println!("test passed: {}", test.name);
175+
anyhow::bail!(
176+
"`ctr run` did not start server or error after two minutes. stderr:\n\t{}",
177+
spin.stderr.output_as_str().unwrap_or("<non-utf8>")
178+
)
179+
}
180+
181+
/// Make an HTTP request against Spin
182+
///
183+
/// Will fail if Spin has already exited or if the io mode is not HTTP
184+
pub fn make_http_request(&mut self, request: Request<'_, String>) -> anyhow::Result<Response> {
185+
let IoMode::Http(port) = self.io_mode;
186+
if let Some(status) = self.try_wait()? {
187+
anyhow::bail!(
188+
"make_http_request - shim exited early with status code {:?}",
189+
status.code()
190+
);
191+
}
192+
log::debug!("Connecting to HTTP server on port {port}...");
193+
let response = request.send("localhost", port).unwrap();
194+
log::debug!("Awaiting response from server");
195+
if let Some(status) = self.try_wait()? {
196+
anyhow::bail!("Spin exited early with status code {:?}", status.code());
197+
}
198+
println!("Response: {}", response.status());
199+
Ok(response)
200+
}
201+
202+
pub fn stderr(&mut self) -> &str {
203+
self.stderr.output_as_str().unwrap_or("<non-utf8>")
204+
}
205+
206+
fn try_wait(&mut self) -> std::io::Result<Option<std::process::ExitStatus>> {
207+
self.process.try_wait()
208+
}
209+
}
210+
211+
impl Drop for SpinShim {
212+
fn drop(&mut self) {
213+
kill_process(&mut self.process);
214+
}
215+
}
216+
217+
impl Runtime for SpinShim {
218+
fn error(&mut self) -> anyhow::Result<()> {
219+
if self.try_wait()?.is_some() {
220+
anyhow::bail!("Containerd shim spin exited early: {}", self.stderr());
221+
}
222+
223+
Ok(())
224+
}
225+
}
226+
227+
/// How this instance is communicating with the outside world
228+
pub enum IoMode {
229+
/// An http server is running on this port
230+
Http(u16),
231+
}
232+
233+
/// Helper function to kill a process
234+
fn kill_process(process: &mut std::process::Child) {
235+
#[cfg(windows)]
236+
{
237+
let _ = process.kill();
238+
}
239+
#[cfg(not(windows))]
240+
{
241+
let pid = nix::unistd::Pid::from_raw(process.id() as i32);
242+
let _ = nix::sys::signal::kill(pid, nix::sys::signal::SIGTERM);
87243
}
88244
}

0 commit comments

Comments
 (0)