1mod fragment;
13mod session;
14
15use proef_core::engine::{
16 DoctorCheck, DoctorResult, EngineFactory, EngineSession, FragmentSupport, PayloadProbeError,
17 ScenarioCtx, StepKindSpec,
18};
19use proef_core::error::EngineError;
20
21pub const EMBEDDED_HURL_VERSION: &str = "8.0.1";
24
25const PROBE_HURL: &str = "GET http://localhost/health\nHTTP 200\n";
27
28const STEP_KINDS: &[StepKindSpec] = &[StepKindSpec {
33 prefix: "hurl",
34 schema: r#"{ "type": "string", "description": "Raw hurl entries; ${…} lowered at author time, {{…}} resolved by hurl at run time" }"#,
35 validate: Some(validate_payload),
36 fragments: Some(FragmentSupport {
37 ext: "hurl",
38 scan: fragment::scan,
39 }),
40}];
41
42pub struct HurlEngineFactory;
44
45impl EngineFactory for HurlEngineFactory {
46 fn id(&self) -> &'static str {
47 "hurl"
48 }
49
50 fn step_kinds(&self) -> &'static [StepKindSpec] {
51 STEP_KINDS
52 }
53
54 fn doctor(&self) -> Vec<DoctorCheck> {
55 vec![
56 DoctorCheck {
57 name: "embedded hurl",
58 run: check_embedded_version,
59 },
60 DoctorCheck {
61 name: "hurl parser",
62 run: check_parser,
63 },
64 DoctorCheck {
65 name: "libcurl",
66 run: check_libcurl,
67 },
68 ]
69 }
70
71 fn open(&self, ctx: &ScenarioCtx) -> Result<Box<dyn EngineSession>, EngineError> {
72 Ok(Box::new(session::HurlSession::open(ctx)?))
73 }
74}
75
76fn check_embedded_version() -> DoctorResult {
78 DoctorResult::pass(format!(
79 "hurl {EMBEDDED_HURL_VERSION} (exact pin, ADR-0003)"
80 ))
81}
82
83fn check_parser() -> DoctorResult {
87 match hurl_core::parser::parse_hurl_file(PROBE_HURL) {
88 Ok(file) if file.entries.len() == 1 => DoctorResult::pass(
89 "parsed 1-entry probe file (hurl_core + libxml2 linkage loads)".to_owned(),
90 ),
91 Ok(file) => DoctorResult::warn(format!(
92 "probe parsed with unexpected entry count {}",
93 file.entries.len()
94 )),
95 Err(err) => DoctorResult::fail(format!("cannot parse probe file: {err:?}")),
96 }
97}
98
99fn validate_payload(text: &str) -> Result<(), PayloadProbeError> {
102 let mut normalized = text.to_owned();
103 if !normalized.ends_with('\n') {
104 normalized.push('\n');
105 }
106 match hurl_core::parser::parse_hurl_file(&normalized) {
107 Ok(file) if file.entries.is_empty() => Err(PayloadProbeError {
111 line: 1,
112 column: 1,
113 message: "contains no hurl entries (only comments or blank lines)".to_owned(),
114 }),
115 Ok(_) => Ok(()),
116 Err(err) => Err(PayloadProbeError {
117 line: err.pos.line,
118 column: err.pos.column,
119 message: format!("{:?}", err.kind),
120 }),
121 }
122}
123
124fn check_libcurl() -> DoctorResult {
127 let info = hurl::http::libcurl_version_info();
128 if info.libraries.is_empty() {
129 return DoctorResult::warn("libcurl loaded but reported no libraries".to_owned());
130 }
131 DoctorResult::pass(format!("{} (host {})", info.libraries.join(" "), info.host))
132}
133
134#[cfg(test)]
135mod tests {
136 #![allow(clippy::unwrap_used)]
137
138 use super::*;
139
140 #[test]
143 fn parser_seam_smoke() {
144 let result = check_parser();
145 assert_eq!(
146 result.status,
147 proef_core::engine::DoctorStatus::Pass,
148 "{}",
149 result.detail
150 );
151 }
152
153 #[test]
154 fn factory_claims_the_hurl_step_kind() {
155 let factory = HurlEngineFactory;
156 assert_eq!(factory.id(), "hurl");
157 assert_eq!(factory.step_kinds().len(), 1);
158 assert_eq!(factory.step_kinds()[0].prefix, "hurl");
159 }
160
161 #[test]
165 fn embedded_version_matches_the_cargo_pin() {
166 let manifest = std::fs::read_to_string(
167 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../Cargo.toml"),
168 )
169 .unwrap();
170 for dep in ["hurl", "hurl_core"] {
171 let needle = format!("{dep} = \"={EMBEDDED_HURL_VERSION}\"");
172 assert!(
173 manifest.contains(&needle),
174 "workspace Cargo.toml must contain `{needle}` (ADR-0003 lockstep)"
175 );
176 }
177 }
178
179 #[test]
180 fn payload_probe_accepts_valid_and_rejects_broken_hurl() {
181 assert!(validate_payload("GET http://x/one\nHTTP 200").is_ok());
182 let err = validate_payload("GET http://x/one\nHTTP 200\n[Wrong]\n").unwrap_err();
184 assert!(
185 err.line >= 2,
186 "position should be near the broken section: {err:?}"
187 );
188 }
189}