mod fragment;
mod session;
use proef_core::engine::{
DoctorCheck, DoctorResult, EngineFactory, EngineSession, FragmentSupport, PayloadProbeError,
RawOption, RawOptionValue, ScenarioCtx, StepKindSpec,
};
use proef_core::error::EngineError;
pub const EMBEDDED_HURL_VERSION: &str = "8.0.1";
const PROBE_HURL: &str = "GET http://localhost/health\nHTTP 200\n";
const STEP_KINDS: &[StepKindSpec] = &[StepKindSpec {
prefix: "hurl",
schema: r#"{ "type": "string", "description": "Raw hurl entries; ${…} lowered at author time, {{…}} resolved by hurl at run time" }"#,
validate: Some(validate_payload),
fragments: Some(FragmentSupport {
ext: "hurl",
scan: fragment::scan,
}),
options: Some(recognise_option),
}];
fn recognise_option(key: &str) -> Option<RawOption> {
let (family, value) = match key {
"retry" => (Some("retry"), Some(RawOptionValue::Count)),
"repeat" => (None, Some(RawOptionValue::Count)),
"delay" => (Some("delay"), Some(RawOptionValue::Duration)),
"retry-interval" => (Some("retry"), None),
_ => return None,
};
Some(RawOption { family, value })
}
pub struct HurlEngineFactory;
impl EngineFactory for HurlEngineFactory {
fn id(&self) -> &'static str {
"hurl"
}
fn step_kinds(&self) -> &'static [StepKindSpec] {
STEP_KINDS
}
fn doctor(&self) -> Vec<DoctorCheck> {
vec![
DoctorCheck {
name: "embedded hurl",
run: check_embedded_version,
},
DoctorCheck {
name: "hurl parser",
run: check_parser,
},
DoctorCheck {
name: "libcurl",
run: check_libcurl,
},
]
}
fn open(&self, ctx: &ScenarioCtx) -> Result<Box<dyn EngineSession>, EngineError> {
Ok(Box::new(session::HurlSession::open(ctx)?))
}
}
fn check_embedded_version() -> DoctorResult {
DoctorResult::pass(format!(
"hurl {EMBEDDED_HURL_VERSION} (exact pin, ADR-0003)"
))
}
fn check_parser() -> DoctorResult {
match hurl_core::parser::parse_hurl_file(PROBE_HURL) {
Ok(file) if file.entries.len() == 1 => DoctorResult::pass(
"parsed 1-entry probe file (hurl_core + libxml2 linkage loads)".to_owned(),
),
Ok(file) => DoctorResult::warn(format!(
"probe parsed with unexpected entry count {}",
file.entries.len()
)),
Err(err) => DoctorResult::fail(format!("cannot parse probe file: {err:?}")),
}
}
fn validate_payload(text: &str) -> Result<(), PayloadProbeError> {
let mut normalized = text.to_owned();
if !normalized.ends_with('\n') {
normalized.push('\n');
}
match hurl_core::parser::parse_hurl_file(&normalized) {
Ok(file) if file.entries.is_empty() => Err(PayloadProbeError {
line: 1,
column: 1,
message: "contains no hurl entries (only comments or blank lines)".to_owned(),
}),
Ok(_) => Ok(()),
Err(err) => Err(PayloadProbeError {
line: err.pos.line,
column: err.pos.column,
message: format!("{:?}", err.kind),
}),
}
}
fn check_libcurl() -> DoctorResult {
let info = hurl::http::libcurl_version_info();
if info.libraries.is_empty() {
return DoctorResult::warn("libcurl loaded but reported no libraries".to_owned());
}
DoctorResult::pass(format!("{} (host {})", info.libraries.join(" "), info.host))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn parser_seam_smoke() {
let result = check_parser();
assert_eq!(
result.status,
proef_core::engine::DoctorStatus::Pass,
"{}",
result.detail
);
}
#[test]
fn factory_claims_the_hurl_step_kind() {
let factory = HurlEngineFactory;
assert_eq!(factory.id(), "hurl");
assert_eq!(factory.step_kinds().len(), 1);
assert_eq!(factory.step_kinds()[0].prefix, "hurl");
}
#[test]
fn embedded_version_matches_the_cargo_pin() {
let manifest = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../Cargo.toml"),
)
.unwrap();
for dep in ["hurl", "hurl_core"] {
let needle = format!("{dep} = \"={EMBEDDED_HURL_VERSION}\"");
assert!(
manifest.contains(&needle),
"workspace Cargo.toml must contain `{needle}` (ADR-0003 lockstep)"
);
}
}
#[test]
fn payload_probe_accepts_valid_and_rejects_broken_hurl() {
assert!(validate_payload("GET http://x/one\nHTTP 200").is_ok());
let err = validate_payload("GET http://x/one\nHTTP 200\n[Wrong]\n").unwrap_err();
assert!(
err.line >= 2,
"position should be near the broken section: {err:?}"
);
}
}