Testing a small http-client on Linux using no_std & embedded reqwless.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Split xtask validate into feature-owned case axes

Refs: reqwl-backend-feature-matrix
Refs: reqwl-dns-mode-features

rektide 9f0eb602 ce29c374

+400 -473
-473
xtask/src/validate.rs
··· 1 - // pattern: Imperative Shell 2 - 3 - use std::path::Path; 4 - use std::process::Command; 5 - 6 - const DNS_IP_ONLY_REJECT_NEEDLE: &str = "dns-ip-only mode accepts literal IP hosts only"; 7 - const HTTP_EXAMPLE_URL: &str = "http://example.com"; 8 - const EMPTY_FEATURES: &[&str] = &[]; 9 - const HTTPS_FEATURES: &[&str] = &["https"]; 10 - 11 - #[derive(Clone, Copy)] 12 - enum BackendFeature { 13 - PosixLibc, 14 - Rustix, 15 - } 16 - 17 - impl BackendFeature { 18 - const fn feature_flag(self) -> &'static str { 19 - match self { 20 - Self::PosixLibc => "backend-posix-libc", 21 - Self::Rustix => "backend-rustix", 22 - } 23 - } 24 - 25 - const fn default_dns(self) -> DnsFeature { 26 - let _ = self; 27 - DnsFeature::GetAddrInfo 28 - } 29 - 30 - fn with_dns(self, dns: DnsFeature) -> FeatureSelection { 31 - TaskFeatures::none() 32 - .with_backend(self) 33 - .with_dns(dns) 34 - .selection() 35 - } 36 - } 37 - 38 - #[derive(Clone, Copy)] 39 - enum DnsFeature { 40 - GetAddrInfo, 41 - IpOnly, 42 - } 43 - 44 - impl DnsFeature { 45 - const fn feature_flag(self) -> &'static str { 46 - match self { 47 - Self::GetAddrInfo => "dns-getaddrinfo", 48 - Self::IpOnly => "dns-ip-only", 49 - } 50 - } 51 - 52 - const fn default_backend(self) -> BackendFeature { 53 - let _ = self; 54 - BackendFeature::PosixLibc 55 - } 56 - 57 - const fn reject_message(self) -> Option<&'static str> { 58 - match self { 59 - Self::IpOnly => Some(DNS_IP_ONLY_REJECT_NEEDLE), 60 - Self::GetAddrInfo => None, 61 - } 62 - } 63 - } 64 - 65 - trait FeatureCommandArgs: Copy { 66 - fn check_display(self) -> String; 67 - fn check_args(self) -> Vec<String>; 68 - fn run_display(self, url: &str) -> String; 69 - fn run_args(self, url: &str) -> Vec<String>; 70 - } 71 - 72 - impl FeatureCommandArgs for BackendFeature { 73 - fn check_display(self) -> String { 74 - TaskFeatures::none() 75 - .with_backend(self) 76 - .selection() 77 - .check_display() 78 - } 79 - 80 - fn check_args(self) -> Vec<String> { 81 - TaskFeatures::none() 82 - .with_backend(self) 83 - .selection() 84 - .check_args() 85 - } 86 - 87 - fn run_display(self, url: &str) -> String { 88 - TaskFeatures::none() 89 - .with_backend(self) 90 - .selection() 91 - .run_display(url) 92 - } 93 - 94 - fn run_args(self, url: &str) -> Vec<String> { 95 - TaskFeatures::none() 96 - .with_backend(self) 97 - .selection() 98 - .run_args(url) 99 - } 100 - } 101 - 102 - impl FeatureCommandArgs for DnsFeature { 103 - fn check_display(self) -> String { 104 - TaskFeatures::none() 105 - .with_dns(self) 106 - .selection() 107 - .check_display() 108 - } 109 - 110 - fn check_args(self) -> Vec<String> { 111 - TaskFeatures::none().with_dns(self).selection().check_args() 112 - } 113 - 114 - fn run_display(self, url: &str) -> String { 115 - TaskFeatures::none() 116 - .with_dns(self) 117 - .selection() 118 - .run_display(url) 119 - } 120 - 121 - fn run_args(self, url: &str) -> Vec<String> { 122 - TaskFeatures::none() 123 - .with_dns(self) 124 - .selection() 125 - .run_args(url) 126 - } 127 - } 128 - 129 - #[derive(Clone, Copy)] 130 - struct TaskFeatures { 131 - backend: Option<BackendFeature>, 132 - dns: Option<DnsFeature>, 133 - extra_features: &'static [&'static str], 134 - } 135 - 136 - impl TaskFeatures { 137 - const fn none() -> Self { 138 - Self { 139 - backend: None, 140 - dns: None, 141 - extra_features: EMPTY_FEATURES, 142 - } 143 - } 144 - 145 - const fn with_backend(self, backend: BackendFeature) -> Self { 146 - Self { 147 - backend: Some(backend), 148 - dns: self.dns, 149 - extra_features: self.extra_features, 150 - } 151 - } 152 - 153 - const fn with_dns(self, dns: DnsFeature) -> Self { 154 - Self { 155 - backend: self.backend, 156 - dns: Some(dns), 157 - extra_features: self.extra_features, 158 - } 159 - } 160 - 161 - const fn with_extras(self, extra_features: &'static [&'static str]) -> Self { 162 - Self { 163 - backend: self.backend, 164 - dns: self.dns, 165 - extra_features, 166 - } 167 - } 168 - 169 - fn selection(self) -> FeatureSelection { 170 - let backend = self 171 - .backend 172 - .or_else(|| self.dns.map(DnsFeature::default_backend)); 173 - let dns = self 174 - .dns 175 - .or_else(|| self.backend.map(BackendFeature::default_dns)); 176 - 177 - let no_default_features = backend.is_some() || dns.is_some(); 178 - 179 - FeatureSelection { 180 - backend, 181 - dns, 182 - extra_features: self.extra_features, 183 - no_default_features, 184 - } 185 - } 186 - } 187 - 188 - #[derive(Clone, Copy)] 189 - struct FeatureSelection { 190 - backend: Option<BackendFeature>, 191 - dns: Option<DnsFeature>, 192 - extra_features: &'static [&'static str], 193 - no_default_features: bool, 194 - } 195 - 196 - impl FeatureSelection { 197 - fn feature_arg(self) -> Option<String> { 198 - let mut features = Vec::new(); 199 - if let Some(backend) = self.backend { 200 - features.push(backend.feature_flag()); 201 - } 202 - if let Some(dns) = self.dns { 203 - features.push(dns.feature_flag()); 204 - } 205 - features.extend(self.extra_features.iter().copied()); 206 - 207 - if features.is_empty() { 208 - return None; 209 - } 210 - Some(features.join(",")) 211 - } 212 - 213 - fn check_display(self) -> String { 214 - match (self.no_default_features, self.feature_arg()) { 215 - (true, Some(features)) => { 216 - format!("cargo check --no-default-features --features {features}") 217 - } 218 - (false, Some(features)) => format!("cargo check --features {features}"), 219 - (_, None) => "cargo check".to_owned(), 220 - } 221 - } 222 - 223 - fn check_args(self) -> Vec<String> { 224 - let mut args = vec!["check".to_owned()]; 225 - if let Some(features) = self.feature_arg() { 226 - if self.no_default_features { 227 - args.push("--no-default-features".to_owned()); 228 - } 229 - args.push("--features".to_owned()); 230 - args.push(features); 231 - } 232 - args 233 - } 234 - 235 - fn run_display(self, url: &str) -> String { 236 - match (self.no_default_features, self.feature_arg()) { 237 - (true, Some(features)) => { 238 - format!("cargo run --no-default-features --features {features} -- {url}") 239 - } 240 - (false, Some(features)) => { 241 - format!("cargo run --features {features} -- {url}") 242 - } 243 - (_, None) => format!("cargo run -- {url}"), 244 - } 245 - } 246 - 247 - fn run_args(self, url: &str) -> Vec<String> { 248 - let mut args = vec!["run".to_owned()]; 249 - if let Some(features) = self.feature_arg() { 250 - if self.no_default_features { 251 - args.push("--no-default-features".to_owned()); 252 - } 253 - args.push("--features".to_owned()); 254 - args.push(features); 255 - } 256 - args.push("--".to_owned()); 257 - args.push(url.to_owned()); 258 - args 259 - } 260 - } 261 - 262 - #[derive(Clone, Copy, clap::ValueEnum)] 263 - pub enum ValidateTask { 264 - #[value(help = "run complete validation suite (build + runtime + guarded failure checks)")] 265 - All, 266 - #[value(help = "run all build-time validations")] 267 - Build, 268 - #[value(help = "run runtime behavior validations")] 269 - Runtime, 270 - #[value(help = "check default feature set builds")] 271 - Check, 272 - #[value(help = "check default feature set with HTTPS enabled")] 273 - CheckHttps, 274 - #[value(help = "check no-default build with posix backend and IP-only DNS mode")] 275 - CheckIpOnly, 276 - #[value(help = "run downloader against example.com in default mode")] 277 - RunHttpExample, 278 - #[value(help = "run IP-only mode against hostname and confirm rejection message")] 279 - RunIpOnlyReject, 280 - #[value(help = "check no-default build with rustix backend and getaddrinfo DNS mode")] 281 - CheckRustix, 282 - #[value(help = "check no-default build with rustix backend and IP-only DNS mode")] 283 - CheckRustixIpOnly, 284 - #[value(help = "run downloader against example.com with rustix backend")] 285 - RunRustixHttpExample, 286 - #[value(help = "run rustix backend in IP-only mode against hostname and confirm rejection")] 287 - RunRustixIpOnlyReject, 288 - } 289 - 290 - impl ValidateTask { 291 - fn features(self) -> Option<TaskFeatures> { 292 - match self { 293 - Self::Check => Some(TaskFeatures::none()), 294 - Self::CheckHttps => Some(TaskFeatures::none().with_extras(HTTPS_FEATURES)), 295 - Self::CheckIpOnly => Some(TaskFeatures::none().with_dns(DnsFeature::IpOnly)), 296 - Self::RunHttpExample => Some(TaskFeatures::none()), 297 - Self::RunIpOnlyReject => Some(TaskFeatures::none().with_dns(DnsFeature::IpOnly)), 298 - Self::CheckRustix => Some(TaskFeatures::none().with_backend(BackendFeature::Rustix)), 299 - Self::CheckRustixIpOnly => Some( 300 - TaskFeatures::none() 301 - .with_backend(BackendFeature::Rustix) 302 - .with_dns(DnsFeature::IpOnly), 303 - ), 304 - Self::RunRustixHttpExample => { 305 - Some(TaskFeatures::none().with_backend(BackendFeature::Rustix)) 306 - } 307 - Self::RunRustixIpOnlyReject => Some( 308 - TaskFeatures::none() 309 - .with_backend(BackendFeature::Rustix) 310 - .with_dns(DnsFeature::IpOnly), 311 - ), 312 - Self::All | Self::Build | Self::Runtime => None, 313 - } 314 - } 315 - } 316 - 317 - pub fn run_validate_tasks(tasks: &[ValidateTask]) -> Result<(), String> { 318 - if tasks.is_empty() { 319 - return run_validate_task(ValidateTask::All); 320 - } 321 - run_sequence(tasks) 322 - } 323 - 324 - fn run_validate_task(task: ValidateTask) -> Result<(), String> { 325 - match task { 326 - ValidateTask::All => run_sequence(&[ 327 - ValidateTask::Check, 328 - ValidateTask::CheckHttps, 329 - ValidateTask::CheckIpOnly, 330 - ValidateTask::CheckRustix, 331 - ValidateTask::CheckRustixIpOnly, 332 - ValidateTask::RunHttpExample, 333 - ValidateTask::RunRustixHttpExample, 334 - ValidateTask::RunIpOnlyReject, 335 - ValidateTask::RunRustixIpOnlyReject, 336 - ]), 337 - ValidateTask::Build => run_sequence(&[ 338 - ValidateTask::Check, 339 - ValidateTask::CheckHttps, 340 - ValidateTask::CheckIpOnly, 341 - ValidateTask::CheckRustix, 342 - ValidateTask::CheckRustixIpOnly, 343 - ]), 344 - ValidateTask::Runtime => run_sequence(&[ 345 - ValidateTask::RunHttpExample, 346 - ValidateTask::RunRustixHttpExample, 347 - ValidateTask::RunIpOnlyReject, 348 - ValidateTask::RunRustixIpOnlyReject, 349 - ]), 350 - ValidateTask::Check => run_check_task(task), 351 - ValidateTask::CheckHttps => run_check_task(task), 352 - ValidateTask::CheckIpOnly => run_check_dns_task(DnsFeature::IpOnly), 353 - ValidateTask::RunHttpExample => run_run_task(task, HTTP_EXAMPLE_URL), 354 - ValidateTask::RunIpOnlyReject => run_ip_only_reject_dns_task(DnsFeature::IpOnly), 355 - ValidateTask::CheckRustix => run_check_backend_task(BackendFeature::Rustix), 356 - ValidateTask::CheckRustixIpOnly => run_check_task(task), 357 - ValidateTask::RunRustixHttpExample => { 358 - run_run_backend_task(BackendFeature::Rustix, HTTP_EXAMPLE_URL) 359 - } 360 - ValidateTask::RunRustixIpOnlyReject => run_ip_only_reject_selection( 361 - BackendFeature::Rustix.with_dns(DnsFeature::IpOnly), 362 - DnsFeature::IpOnly, 363 - ), 364 - } 365 - } 366 - 367 - fn run_check_task(task: ValidateTask) -> Result<(), String> { 368 - let selection = task 369 - .features() 370 - .ok_or_else(|| "task has no feature selection".to_owned())? 371 - .selection(); 372 - run_success(&selection.check_display(), &selection.check_args()) 373 - } 374 - 375 - fn run_run_task(task: ValidateTask, url: &str) -> Result<(), String> { 376 - let selection = task 377 - .features() 378 - .ok_or_else(|| "task has no feature selection".to_owned())? 379 - .selection(); 380 - run_success(&selection.run_display(url), &selection.run_args(url)) 381 - } 382 - 383 - fn run_check_backend_task(backend: BackendFeature) -> Result<(), String> { 384 - run_success(&backend.check_display(), &backend.check_args()) 385 - } 386 - 387 - fn run_run_backend_task(backend: BackendFeature, url: &str) -> Result<(), String> { 388 - run_success(&backend.run_display(url), &backend.run_args(url)) 389 - } 390 - 391 - fn run_check_dns_task(dns: DnsFeature) -> Result<(), String> { 392 - run_success(&dns.check_display(), &dns.check_args()) 393 - } 394 - 395 - fn run_ip_only_reject_dns_task(dns: DnsFeature) -> Result<(), String> { 396 - let selection = TaskFeatures::none().with_dns(dns).selection(); 397 - run_ip_only_reject_selection(selection, dns) 398 - } 399 - 400 - fn run_ip_only_reject_selection( 401 - selection: FeatureSelection, 402 - dns: DnsFeature, 403 - ) -> Result<(), String> { 404 - let needle = dns 405 - .reject_message() 406 - .ok_or_else(|| "dns feature has no reject expectation".to_owned())?; 407 - run_expected_failure( 408 - &selection.run_display(HTTP_EXAMPLE_URL), 409 - &selection.run_args(HTTP_EXAMPLE_URL), 410 - needle, 411 - ) 412 - } 413 - 414 - fn run_sequence(tasks: &[ValidateTask]) -> Result<(), String> { 415 - for task in tasks { 416 - run_validate_task(*task)?; 417 - } 418 - Ok(()) 419 - } 420 - 421 - fn run_success(display: &str, cargo_args: &[String]) -> Result<(), String> { 422 - eprintln!("==> {display}"); 423 - let output = run_cargo(cargo_args)?; 424 - if output.status.success() { 425 - return Ok(()); 426 - } 427 - Err(format_command_failure( 428 - display, 429 - &output.stdout, 430 - &output.stderr, 431 - )) 432 - } 433 - 434 - fn run_expected_failure(display: &str, cargo_args: &[String], needle: &str) -> Result<(), String> { 435 - eprintln!("==> {display}"); 436 - let output = run_cargo(cargo_args)?; 437 - if output.status.success() { 438 - return Err(format!("expected failure but command succeeded: {display}")); 439 - } 440 - 441 - let stdout = String::from_utf8_lossy(&output.stdout); 442 - let stderr = String::from_utf8_lossy(&output.stderr); 443 - if stdout.contains(needle) || stderr.contains(needle) { 444 - return Ok(()); 445 - } 446 - 447 - Err(format!( 448 - "command failed as expected but missing required message: {needle}\n{}", 449 - format_command_failure(display, &output.stdout, &output.stderr) 450 - )) 451 - } 452 - 453 - fn run_cargo(cargo_args: &[String]) -> Result<std::process::Output, String> { 454 - Command::new("cargo") 455 - .current_dir(workspace_root()) 456 - .args(cargo_args) 457 - .output() 458 - .map_err(|error| format!("failed to execute cargo: {error}")) 459 - } 460 - 461 - fn workspace_root() -> &'static Path { 462 - Path::new(env!("CARGO_MANIFEST_DIR")) 463 - .parent() 464 - .expect("xtask crate must live directly under workspace root") 465 - } 466 - 467 - fn format_command_failure(display: &str, stdout: &[u8], stderr: &[u8]) -> String { 468 - format!( 469 - "command failed: {display}\nstdout:\n{}\nstderr:\n{}", 470 - String::from_utf8_lossy(stdout), 471 - String::from_utf8_lossy(stderr) 472 - ) 473 - }
+60
xtask/src/validate/axes/dns.rs
··· 1 + use super::PlatformFeature; 2 + use crate::validate::case::{ 3 + build_check_case, build_run_case, CaseAxis, CaseSpec, Expectation, Scenario, 4 + DNS_IP_ONLY_REJECT_NEEDLE, 5 + }; 6 + 7 + #[derive(Clone, Copy)] 8 + pub enum DnsFeature { 9 + GetAddrInfo, 10 + IpOnly, 11 + } 12 + 13 + impl DnsFeature { 14 + fn reject_expectation(self) -> Option<Expectation> { 15 + match self { 16 + Self::GetAddrInfo => None, 17 + Self::IpOnly => Some(Expectation::FailureContains(DNS_IP_ONLY_REJECT_NEEDLE)), 18 + } 19 + } 20 + } 21 + 22 + impl CaseAxis for DnsFeature { 23 + type Peer = PlatformFeature; 24 + 25 + fn flag(self) -> &'static str { 26 + match self { 27 + Self::GetAddrInfo => "dns-getaddrinfo", 28 + Self::IpOnly => "dns-ip-only", 29 + } 30 + } 31 + 32 + fn default_peer(self) -> Self::Peer { 33 + let _ = self; 34 + PlatformFeature::PosixLibc 35 + } 36 + 37 + fn case(self, scenario: Scenario, peer: Option<Self::Peer>) -> Option<CaseSpec> { 38 + let platform = peer.unwrap_or(self.default_peer()); 39 + let features = [platform.flag(), self.flag()]; 40 + 41 + match scenario { 42 + Scenario::Check => Some(build_check_case(&features, true)), 43 + Scenario::RunExample => Some(build_run_case( 44 + &features, 45 + true, 46 + crate::validate::case::HTTP_EXAMPLE_URL, 47 + Expectation::Success, 48 + )), 49 + Scenario::RejectHostname => { 50 + let expectation = self.reject_expectation()?; 51 + Some(build_run_case( 52 + &features, 53 + true, 54 + crate::validate::case::HTTP_EXAMPLE_URL, 55 + expectation, 56 + )) 57 + } 58 + } 59 + } 60 + }
+5
xtask/src/validate/axes/mod.rs
··· 1 + pub mod dns; 2 + pub mod platform; 3 + 4 + pub use dns::DnsFeature; 5 + pub use platform::PlatformFeature;
+42
xtask/src/validate/axes/platform.rs
··· 1 + use super::DnsFeature; 2 + use crate::validate::case::{ 3 + build_check_case, build_run_case, CaseAxis, CaseSpec, Expectation, Scenario, 4 + }; 5 + 6 + #[derive(Clone, Copy)] 7 + pub enum PlatformFeature { 8 + PosixLibc, 9 + Rustix, 10 + } 11 + 12 + impl CaseAxis for PlatformFeature { 13 + type Peer = DnsFeature; 14 + 15 + fn flag(self) -> &'static str { 16 + match self { 17 + Self::PosixLibc => "backend-posix-libc", 18 + Self::Rustix => "backend-rustix", 19 + } 20 + } 21 + 22 + fn default_peer(self) -> Self::Peer { 23 + let _ = self; 24 + DnsFeature::GetAddrInfo 25 + } 26 + 27 + fn case(self, scenario: Scenario, peer: Option<Self::Peer>) -> Option<CaseSpec> { 28 + let dns = peer.unwrap_or(self.default_peer()); 29 + let features = [self.flag(), dns.flag()]; 30 + 31 + match scenario { 32 + Scenario::Check => Some(build_check_case(&features, true)), 33 + Scenario::RunExample => Some(build_run_case( 34 + &features, 35 + true, 36 + crate::validate::case::HTTP_EXAMPLE_URL, 37 + Expectation::Success, 38 + )), 39 + Scenario::RejectHostname => None, 40 + } 41 + } 42 + }
+102
xtask/src/validate/case.rs
··· 1 + pub const HTTP_EXAMPLE_URL: &str = "http://example.com"; 2 + pub const DNS_IP_ONLY_REJECT_NEEDLE: &str = "dns-ip-only mode accepts literal IP hosts only"; 3 + 4 + #[derive(Clone, Copy)] 5 + pub enum Scenario { 6 + Check, 7 + RunExample, 8 + RejectHostname, 9 + } 10 + 11 + #[derive(Clone)] 12 + pub enum Expectation { 13 + Success, 14 + FailureContains(&'static str), 15 + } 16 + 17 + #[derive(Clone)] 18 + pub struct CaseSpec { 19 + pub display: String, 20 + pub args: Vec<String>, 21 + pub expectation: Expectation, 22 + } 23 + 24 + pub trait CaseAxis: Copy { 25 + type Peer: CaseAxis<Peer = Self>; 26 + 27 + fn flag(self) -> &'static str; 28 + fn default_peer(self) -> Self::Peer; 29 + fn case(self, scenario: Scenario, peer: Option<Self::Peer>) -> Option<CaseSpec>; 30 + } 31 + 32 + pub fn default_check_case() -> CaseSpec { 33 + build_check_case(&[], false) 34 + } 35 + 36 + pub fn default_run_example_case() -> CaseSpec { 37 + build_run_case(&[], false, HTTP_EXAMPLE_URL, Expectation::Success) 38 + } 39 + 40 + pub fn https_check_case() -> CaseSpec { 41 + build_check_case(&["https"], false) 42 + } 43 + 44 + pub fn build_check_case(features: &[&str], no_default_features: bool) -> CaseSpec { 45 + let args = build_check_args(features, no_default_features); 46 + let display = format_command("cargo", &args); 47 + CaseSpec { 48 + display, 49 + args, 50 + expectation: Expectation::Success, 51 + } 52 + } 53 + 54 + pub fn build_run_case( 55 + features: &[&str], 56 + no_default_features: bool, 57 + url: &str, 58 + expectation: Expectation, 59 + ) -> CaseSpec { 60 + let args = build_run_args(features, no_default_features, url); 61 + let display = format_command("cargo", &args); 62 + CaseSpec { 63 + display, 64 + args, 65 + expectation, 66 + } 67 + } 68 + 69 + fn build_check_args(features: &[&str], no_default_features: bool) -> Vec<String> { 70 + let mut args = vec!["check".to_owned()]; 71 + if !features.is_empty() { 72 + if no_default_features { 73 + args.push("--no-default-features".to_owned()); 74 + } 75 + args.push("--features".to_owned()); 76 + args.push(features.join(",")); 77 + } 78 + args 79 + } 80 + 81 + fn build_run_args(features: &[&str], no_default_features: bool, url: &str) -> Vec<String> { 82 + let mut args = vec!["run".to_owned()]; 83 + if !features.is_empty() { 84 + if no_default_features { 85 + args.push("--no-default-features".to_owned()); 86 + } 87 + args.push("--features".to_owned()); 88 + args.push(features.join(",")); 89 + } 90 + args.push("--".to_owned()); 91 + args.push(url.to_owned()); 92 + args 93 + } 94 + 95 + fn format_command(command: &str, args: &[String]) -> String { 96 + let mut display = String::from(command); 97 + for arg in args { 98 + display.push(' '); 99 + display.push_str(arg); 100 + } 101 + display 102 + }
+36
xtask/src/validate/mod.rs
··· 1 + // pattern: Imperative Shell 2 + 3 + mod axes; 4 + mod case; 5 + mod runner; 6 + mod task; 7 + 8 + pub use task::ValidateTask; 9 + 10 + use runner::run_case; 11 + 12 + pub fn run_validate_tasks(tasks: &[ValidateTask]) -> Result<(), String> { 13 + if tasks.is_empty() { 14 + return run_validate_task(ValidateTask::All); 15 + } 16 + 17 + run_sequence(tasks) 18 + } 19 + 20 + fn run_sequence(tasks: &[ValidateTask]) -> Result<(), String> { 21 + for task in tasks { 22 + run_validate_task(*task)?; 23 + } 24 + Ok(()) 25 + } 26 + 27 + fn run_validate_task(task: ValidateTask) -> Result<(), String> { 28 + if let Some(group) = task.group_tasks() { 29 + return run_sequence(group); 30 + } 31 + 32 + let case = task 33 + .case_spec() 34 + .ok_or_else(|| format!("validate task has no case specification: {task:?}"))?; 35 + run_case(&case) 36 + }
+64
xtask/src/validate/runner.rs
··· 1 + use std::path::Path; 2 + use std::process::Command; 3 + 4 + use crate::validate::case::{CaseSpec, Expectation}; 5 + 6 + pub fn run_case(case: &CaseSpec) -> Result<(), String> { 7 + eprintln!("==> {}", case.display); 8 + let output = run_cargo(&case.args)?; 9 + 10 + match &case.expectation { 11 + Expectation::Success => { 12 + if output.status.success() { 13 + Ok(()) 14 + } else { 15 + Err(format_command_failure( 16 + &case.display, 17 + &output.stdout, 18 + &output.stderr, 19 + )) 20 + } 21 + } 22 + Expectation::FailureContains(needle) => { 23 + if output.status.success() { 24 + return Err(format!( 25 + "expected failure but command succeeded: {}", 26 + case.display 27 + )); 28 + } 29 + 30 + let stdout = String::from_utf8_lossy(&output.stdout); 31 + let stderr = String::from_utf8_lossy(&output.stderr); 32 + if stdout.contains(needle) || stderr.contains(needle) { 33 + return Ok(()); 34 + } 35 + 36 + Err(format!( 37 + "command failed as expected but missing required message: {needle}\n{}", 38 + format_command_failure(&case.display, &output.stdout, &output.stderr) 39 + )) 40 + } 41 + } 42 + } 43 + 44 + fn run_cargo(cargo_args: &[String]) -> Result<std::process::Output, String> { 45 + Command::new("cargo") 46 + .current_dir(workspace_root()) 47 + .args(cargo_args) 48 + .output() 49 + .map_err(|error| format!("failed to execute cargo: {error}")) 50 + } 51 + 52 + fn workspace_root() -> &'static Path { 53 + Path::new(env!("CARGO_MANIFEST_DIR")) 54 + .parent() 55 + .expect("xtask crate must live directly under workspace root") 56 + } 57 + 58 + fn format_command_failure(display: &str, stdout: &[u8], stderr: &[u8]) -> String { 59 + format!( 60 + "command failed: {display}\nstdout:\n{}\nstderr:\n{}", 61 + String::from_utf8_lossy(stdout), 62 + String::from_utf8_lossy(stderr) 63 + ) 64 + }
+91
xtask/src/validate/task.rs
··· 1 + use crate::validate::axes::{DnsFeature, PlatformFeature}; 2 + use crate::validate::case::{ 3 + default_check_case, default_run_example_case, https_check_case, CaseAxis, CaseSpec, Scenario, 4 + }; 5 + 6 + #[derive(Clone, Copy, Debug, clap::ValueEnum)] 7 + pub enum ValidateTask { 8 + #[value(help = "run complete validation suite (build + runtime + guarded failure checks)")] 9 + All, 10 + #[value(help = "run all build-time validations")] 11 + Build, 12 + #[value(help = "run runtime behavior validations")] 13 + Runtime, 14 + #[value(help = "check default feature set builds")] 15 + Check, 16 + #[value(help = "check default feature set with HTTPS enabled")] 17 + CheckHttps, 18 + #[value(help = "check no-default build with posix backend and IP-only DNS mode")] 19 + CheckIpOnly, 20 + #[value(help = "run downloader against example.com in default mode")] 21 + RunHttpExample, 22 + #[value(help = "run IP-only mode against hostname and confirm rejection message")] 23 + RunIpOnlyReject, 24 + #[value(help = "check no-default build with rustix backend and getaddrinfo DNS mode")] 25 + CheckRustix, 26 + #[value(help = "check no-default build with rustix backend and IP-only DNS mode")] 27 + CheckRustixIpOnly, 28 + #[value(help = "run downloader against example.com with rustix backend")] 29 + RunRustixHttpExample, 30 + #[value(help = "run rustix backend in IP-only mode against hostname and confirm rejection")] 31 + RunRustixIpOnlyReject, 32 + } 33 + 34 + impl ValidateTask { 35 + pub(crate) fn group_tasks(self) -> Option<&'static [ValidateTask]> { 36 + match self { 37 + Self::All => Some(&[ 38 + Self::Check, 39 + Self::CheckHttps, 40 + Self::CheckIpOnly, 41 + Self::CheckRustix, 42 + Self::CheckRustixIpOnly, 43 + Self::RunHttpExample, 44 + Self::RunRustixHttpExample, 45 + Self::RunIpOnlyReject, 46 + Self::RunRustixIpOnlyReject, 47 + ]), 48 + Self::Build => Some(&[ 49 + Self::Check, 50 + Self::CheckHttps, 51 + Self::CheckIpOnly, 52 + Self::CheckRustix, 53 + Self::CheckRustixIpOnly, 54 + ]), 55 + Self::Runtime => Some(&[ 56 + Self::RunHttpExample, 57 + Self::RunRustixHttpExample, 58 + Self::RunIpOnlyReject, 59 + Self::RunRustixIpOnlyReject, 60 + ]), 61 + _ => None, 62 + } 63 + } 64 + 65 + pub(crate) fn case_spec(self) -> Option<CaseSpec> { 66 + match self { 67 + Self::Check => Some(default_check_case()), 68 + Self::CheckHttps => Some(https_check_case()), 69 + Self::CheckIpOnly => { 70 + DnsFeature::IpOnly.case(Scenario::Check, Some(PlatformFeature::PosixLibc)) 71 + } 72 + Self::RunHttpExample => Some(default_run_example_case()), 73 + Self::RunIpOnlyReject => { 74 + DnsFeature::IpOnly.case(Scenario::RejectHostname, Some(PlatformFeature::PosixLibc)) 75 + } 76 + Self::CheckRustix => { 77 + PlatformFeature::Rustix.case(Scenario::Check, Some(DnsFeature::GetAddrInfo)) 78 + } 79 + Self::CheckRustixIpOnly => { 80 + PlatformFeature::Rustix.case(Scenario::Check, Some(DnsFeature::IpOnly)) 81 + } 82 + Self::RunRustixHttpExample => { 83 + PlatformFeature::Rustix.case(Scenario::RunExample, Some(DnsFeature::GetAddrInfo)) 84 + } 85 + Self::RunRustixIpOnlyReject => { 86 + DnsFeature::IpOnly.case(Scenario::RejectHostname, Some(PlatformFeature::Rustix)) 87 + } 88 + Self::All | Self::Build | Self::Runtime => None, 89 + } 90 + } 91 + }