Skip to content

Commit c58a739

Browse files
committed
test(pxe): cover config and extractor scenarios
Add carbide-test-support tables for runtime env parsing, machine architecture parsing, PXE request errors, and logfmt rendering. That extends the existing PXE tests and moves line coverage from 36.10% to 52.66%. Signed-off-by: Chet Nichols III <chetn@nvidia.com>
1 parent e61fb24 commit c58a739

6 files changed

Lines changed: 385 additions & 14 deletions

File tree

Cargo.lock

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

crates/pxe/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ uuid = { features = ["v4", "serde"], workspace = true }
6161
carbide-version = { path = "../version" }
6262

6363
[dev-dependencies]
64+
carbide-test-support = { path = "../test-support" }
6465
tempfile = { workspace = true }
6566

6667
[lints]

crates/pxe/src/config.rs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,213 @@ impl RuntimeConfig {
6767
Ok(this)
6868
}
6969
}
70+
71+
#[cfg(test)]
72+
mod tests {
73+
use std::net::IpAddr;
74+
use std::sync::Mutex;
75+
76+
use carbide_test_support::Outcome::*;
77+
use carbide_test_support::scenarios;
78+
79+
use super::*;
80+
81+
static ENV_LOCK: Mutex<()> = Mutex::new(());
82+
const RUNTIME_CONFIG_ENV_KEYS: &[&str] = &[
83+
"CARBIDE_PXE_URL",
84+
"CARBIDE_API_INTERNAL_URL",
85+
"CARBIDE_API_URL",
86+
"CARBIDE_STATIC_PXE_URL",
87+
"FORGE_ROOT_CAFILE_PATH",
88+
"FORGE_CLIENT_CERT_PATH",
89+
"FORGE_CLIENT_KEY_PATH",
90+
"PXE_BIND_ADDRESS",
91+
"PXE_BIND_PORT",
92+
"CARBIDE_PXE_TEMPLATE_DIRECTORY",
93+
];
94+
95+
#[derive(Debug)]
96+
struct ConfigEnv {
97+
vars: &'static [(&'static str, &'static str)],
98+
}
99+
100+
#[derive(Debug, PartialEq)]
101+
struct RuntimeConfigSummary {
102+
internal_api_url: String,
103+
client_facing_api_url: String,
104+
pxe_url: String,
105+
static_pxe_url: String,
106+
forge_root_ca_path: String,
107+
server_cert_path: String,
108+
server_key_path: String,
109+
bind_address: IpAddr,
110+
bind_port: u16,
111+
template_directory: String,
112+
}
113+
114+
struct EnvSnapshot {
115+
values: Vec<(&'static str, Option<String>)>,
116+
}
117+
118+
impl EnvSnapshot {
119+
fn capture(keys: &[&'static str]) -> Self {
120+
Self {
121+
values: keys.iter().map(|key| (*key, env::var(key).ok())).collect(),
122+
}
123+
}
124+
}
125+
126+
impl Drop for EnvSnapshot {
127+
fn drop(&mut self) {
128+
for (key, value) in &self.values {
129+
match value {
130+
Some(value) => set_env(key, value),
131+
None => remove_env(key),
132+
}
133+
}
134+
}
135+
}
136+
137+
fn set_env(key: &str, value: &str) {
138+
// SAFETY: these tests hold ENV_LOCK while mutating process environment.
139+
unsafe { env::set_var(key, value) }
140+
}
141+
142+
fn remove_env(key: &str) {
143+
// SAFETY: these tests hold ENV_LOCK while mutating process environment.
144+
unsafe { env::remove_var(key) }
145+
}
146+
147+
fn clear_env(keys: &[&str]) {
148+
for key in keys {
149+
remove_env(key);
150+
}
151+
}
152+
153+
fn summarize_config(config: RuntimeConfig) -> RuntimeConfigSummary {
154+
RuntimeConfigSummary {
155+
internal_api_url: config.internal_api_url,
156+
client_facing_api_url: config.client_facing_api_url,
157+
pxe_url: config.pxe_url,
158+
static_pxe_url: config.static_pxe_url,
159+
forge_root_ca_path: config.forge_root_ca_path,
160+
server_cert_path: config.server_cert_path,
161+
server_key_path: config.server_key_path,
162+
bind_address: config.bind_address,
163+
bind_port: config.bind_port,
164+
template_directory: config.template_directory,
165+
}
166+
}
167+
168+
/// Caller must hold `ENV_LOCK` before invoking this function.
169+
fn runtime_config_from_env(input: ConfigEnv) -> Result<RuntimeConfigSummary, &'static str> {
170+
clear_env(RUNTIME_CONFIG_ENV_KEYS);
171+
for (key, value) in input.vars {
172+
set_env(key, value);
173+
}
174+
RuntimeConfig::from_env()
175+
.map(summarize_config)
176+
.map_err(config_error_kind)
177+
}
178+
179+
fn config_error_kind(error: String) -> &'static str {
180+
match error.as_str() {
181+
"Could not extract FORGE_ROOT_CAFILE_PATH from environment" => "missing-root-ca",
182+
"Could not extract FORGE_CLIENT_CERT_PATH from environment" => "missing-client-cert",
183+
"Could not extract FORGE_CLIENT_KEY_PATH from environment" => "missing-client-key",
184+
"not a parsable bind address for runtime config?" => "bad-bind-address",
185+
"not a parsable bind port for runtime config?" => "bad-bind-port",
186+
other => panic!("unmapped runtime config error: {other:?}"),
187+
}
188+
}
189+
190+
#[test]
191+
fn builds_runtime_config_from_environment() {
192+
let _lock = ENV_LOCK.lock().unwrap_or_else(|error| error.into_inner());
193+
let _snapshot = EnvSnapshot::capture(RUNTIME_CONFIG_ENV_KEYS);
194+
195+
scenarios!(
196+
run = runtime_config_from_env;
197+
"required values with defaults" {
198+
ConfigEnv {
199+
vars: &[
200+
("FORGE_ROOT_CAFILE_PATH", "/certs/root.pem"),
201+
("FORGE_CLIENT_CERT_PATH", "/certs/client.pem"),
202+
("FORGE_CLIENT_KEY_PATH", "/certs/client.key"),
203+
],
204+
} => Yields(RuntimeConfigSummary {
205+
internal_api_url: "https://carbide-api.forge-system.svc.cluster.local:1079".to_string(),
206+
client_facing_api_url: "https://carbide-api.forge".to_string(),
207+
pxe_url: "http://carbide-pxe.forge".to_string(),
208+
static_pxe_url: "http://carbide-pxe.forge".to_string(),
209+
forge_root_ca_path: "/certs/root.pem".to_string(),
210+
server_cert_path: "/certs/client.pem".to_string(),
211+
server_key_path: "/certs/client.key".to_string(),
212+
bind_address: "0.0.0.0".parse().unwrap(),
213+
bind_port: 8080,
214+
template_directory: "/opt/carbide/pxe/templates".to_string(),
215+
}),
216+
}
217+
218+
"explicit values" {
219+
ConfigEnv {
220+
vars: &[
221+
("CARBIDE_API_INTERNAL_URL", "https://internal.example.com"),
222+
("CARBIDE_API_URL", "https://client.example.com"),
223+
("CARBIDE_PXE_URL", "http://pxe.example.com"),
224+
("CARBIDE_STATIC_PXE_URL", "http://static-pxe.example.com"),
225+
("FORGE_ROOT_CAFILE_PATH", "/explicit/root.pem"),
226+
("FORGE_CLIENT_CERT_PATH", "/explicit/client.pem"),
227+
("FORGE_CLIENT_KEY_PATH", "/explicit/client.key"),
228+
("PXE_BIND_ADDRESS", "127.0.0.1"),
229+
("PXE_BIND_PORT", "9090"),
230+
("CARBIDE_PXE_TEMPLATE_DIRECTORY", "/templates"),
231+
],
232+
} => Yields(RuntimeConfigSummary {
233+
internal_api_url: "https://internal.example.com".to_string(),
234+
client_facing_api_url: "https://client.example.com".to_string(),
235+
pxe_url: "http://pxe.example.com".to_string(),
236+
static_pxe_url: "http://static-pxe.example.com".to_string(),
237+
forge_root_ca_path: "/explicit/root.pem".to_string(),
238+
server_cert_path: "/explicit/client.pem".to_string(),
239+
server_key_path: "/explicit/client.key".to_string(),
240+
bind_address: "127.0.0.1".parse().unwrap(),
241+
bind_port: 9090,
242+
template_directory: "/templates".to_string(),
243+
}),
244+
}
245+
246+
"missing required values" {
247+
ConfigEnv { vars: &[] } => FailsWith("missing-root-ca"),
248+
ConfigEnv {
249+
vars: &[("FORGE_ROOT_CAFILE_PATH", "/certs/root.pem")],
250+
} => FailsWith("missing-client-cert"),
251+
ConfigEnv {
252+
vars: &[
253+
("FORGE_ROOT_CAFILE_PATH", "/certs/root.pem"),
254+
("FORGE_CLIENT_CERT_PATH", "/certs/client.pem"),
255+
],
256+
} => FailsWith("missing-client-key"),
257+
}
258+
259+
"invalid bind settings" {
260+
ConfigEnv {
261+
vars: &[
262+
("FORGE_ROOT_CAFILE_PATH", "/certs/root.pem"),
263+
("FORGE_CLIENT_CERT_PATH", "/certs/client.pem"),
264+
("FORGE_CLIENT_KEY_PATH", "/certs/client.key"),
265+
("PXE_BIND_ADDRESS", "not an ip"),
266+
],
267+
} => FailsWith("bad-bind-address"),
268+
ConfigEnv {
269+
vars: &[
270+
("FORGE_ROOT_CAFILE_PATH", "/certs/root.pem"),
271+
("FORGE_CLIENT_CERT_PATH", "/certs/client.pem"),
272+
("FORGE_CLIENT_KEY_PATH", "/certs/client.key"),
273+
("PXE_BIND_PORT", "not a port"),
274+
],
275+
} => FailsWith("bad-bind-port"),
276+
}
277+
);
278+
}
279+
}

crates/pxe/src/extractors/machine_architecture.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,55 @@ impl From<MachineArchitecture> for rpc::MachineArchitecture {
5353
}
5454
}
5555
}
56+
57+
#[cfg(test)]
58+
mod tests {
59+
use carbide_test_support::Outcome::*;
60+
use carbide_test_support::{scenarios, value_scenarios};
61+
62+
use super::*;
63+
64+
fn parse_arch(value: &str) -> Result<&'static str, &'static str> {
65+
MachineArchitecture::try_from(value)
66+
.map(|arch| match arch {
67+
MachineArchitecture::Arm => "arm",
68+
MachineArchitecture::X86 => "x86",
69+
})
70+
.map_err(|error| match error {
71+
PxeRequestError::MalformedBuildArch(_) => "malformed-build-arch",
72+
_ => "unexpected",
73+
})
74+
}
75+
76+
#[test]
77+
fn parses_machine_architecture_identifiers() {
78+
scenarios!(parse_arch:
79+
"named identifiers" {
80+
"arm64" => Yields("arm"),
81+
"x86_64" => Yields("x86"),
82+
}
83+
84+
"numeric identifiers" {
85+
"0" => Yields("arm"),
86+
"1" => Yields("x86"),
87+
}
88+
89+
"invalid identifiers" {
90+
"" => FailsWith("malformed-build-arch"),
91+
"amd64" => FailsWith("malformed-build-arch"),
92+
"2" => FailsWith("malformed-build-arch"),
93+
}
94+
);
95+
}
96+
97+
#[test]
98+
fn converts_machine_architecture_to_rpc_enum() {
99+
value_scenarios!(
100+
run = |arch| rpc::MachineArchitecture::from(arch) as i32;
101+
"rpc enum" {
102+
MachineArchitecture::Arm => rpc::MachineArchitecture::Arm as i32,
103+
MachineArchitecture::X86 => rpc::MachineArchitecture::X86 as i32,
104+
}
105+
);
106+
}
107+
}

crates/pxe/src/middleware/logging.rs

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -113,24 +113,46 @@ fn render_logfmt(props: &BTreeMap<&'static str, String>) -> String {
113113

114114
#[cfg(test)]
115115
mod tests {
116+
use carbide_test_support::value_scenarios;
117+
116118
use super::*;
117119

118-
#[test]
119-
fn test_logfmt() {
120+
fn render(entries: Vec<(&'static str, &'static str)>) -> String {
120121
let mut props = BTreeMap::new();
121-
props.insert("method", "GET".to_string());
122-
props.insert("path", "/boot".to_string());
123-
props.insert("remote_ip", "127.0.0.1".to_string());
124-
assert_eq!(
125-
render_logfmt(&props),
126-
"method=GET path=/boot remote_ip=127.0.0.1"
127-
);
122+
for (key, value) in entries {
123+
props.insert(key, value.to_string());
124+
}
125+
render_logfmt(&props)
126+
}
128127

129-
props.insert("z", "with whitespace".to_string());
130-
props.insert("e", "".to_string());
131-
assert_eq!(
132-
render_logfmt(&props),
133-
"e=\"\" method=GET path=/boot remote_ip=127.0.0.1 z=\"with whitespace\""
128+
#[test]
129+
fn renders_logfmt() {
130+
value_scenarios!(
131+
render:
132+
"plain values" {
133+
vec![
134+
("method", "GET"),
135+
("path", "/boot"),
136+
("remote_ip", "127.0.0.1"),
137+
] => "method=GET path=/boot remote_ip=127.0.0.1".to_string(),
138+
}
139+
140+
"quoted values" {
141+
vec![
142+
("method", "GET"),
143+
("path", "/boot"),
144+
("remote_ip", "127.0.0.1"),
145+
("z", "with whitespace"),
146+
("e", ""),
147+
] => "e=\"\" method=GET path=/boot remote_ip=127.0.0.1 z=\"with whitespace\"".to_string(),
148+
}
149+
150+
"escaped values" {
151+
vec![
152+
("message", "quoted \"value\""),
153+
("path", "a=b"),
154+
] => "message=\"quoted \\\"value\\\"\" path=\"a=b\"".to_string(),
155+
}
134156
);
135157
}
136158
}

0 commit comments

Comments
 (0)