1mod session;
13
14use proef_core::engine::{
15 DoctorCheck, DoctorResult, EngineFactory, EngineSession, PayloadProbeError, ScenarioCtx,
16 StepKindSpec,
17};
18use proef_core::error::EngineError;
19
20pub const EMBEDDED_HURL_VERSION: &str = "8.0.1";
23
24const PROBE_HURL: &str = "GET http://localhost/health\nHTTP 200\n";
26
27const STEP_KINDS: &[StepKindSpec] = &[StepKindSpec {
31 prefix: "hurl",
32 schema: r#"{ "type": "string", "description": "Raw hurl entries; ${…} lowered at author time, {{…}} resolved by hurl at run time" }"#,
33 validate: Some(validate_payload),
34}];
35
36pub struct HurlEngineFactory;
38
39impl EngineFactory for HurlEngineFactory {
40 fn id(&self) -> &'static str {
41 "hurl"
42 }
43
44 fn step_kinds(&self) -> &'static [StepKindSpec] {
45 STEP_KINDS
46 }
47
48 fn doctor(&self) -> Vec<DoctorCheck> {
49 vec![
50 DoctorCheck {
51 name: "embedded hurl",
52 run: check_embedded_version,
53 },
54 DoctorCheck {
55 name: "hurl parser",
56 run: check_parser,
57 },
58 DoctorCheck {
59 name: "libcurl",
60 run: check_libcurl,
61 },
62 ]
63 }
64
65 fn open(&self, ctx: &ScenarioCtx) -> Result<Box<dyn EngineSession>, EngineError> {
66 Ok(Box::new(session::HurlSession::open(ctx)?))
67 }
68}
69
70fn check_embedded_version() -> DoctorResult {
72 DoctorResult::pass(format!(
73 "hurl {EMBEDDED_HURL_VERSION} (exact pin, ADR-0003)"
74 ))
75}
76
77fn check_parser() -> DoctorResult {
81 match hurl_core::parser::parse_hurl_file(PROBE_HURL) {
82 Ok(file) if file.entries.len() == 1 => DoctorResult::pass(
83 "parsed 1-entry probe file (hurl_core + libxml2 linkage loads)".to_owned(),
84 ),
85 Ok(file) => DoctorResult::warn(format!(
86 "probe parsed with unexpected entry count {}",
87 file.entries.len()
88 )),
89 Err(err) => DoctorResult::fail(format!("cannot parse probe file: {err:?}")),
90 }
91}
92
93fn validate_payload(text: &str) -> Result<(), PayloadProbeError> {
96 let mut normalized = text.to_owned();
97 if !normalized.ends_with('\n') {
98 normalized.push('\n');
99 }
100 match hurl_core::parser::parse_hurl_file(&normalized) {
101 Ok(file) if file.entries.is_empty() => Err(PayloadProbeError {
105 line: 1,
106 column: 1,
107 message: "contains no hurl entries (only comments or blank lines)".to_owned(),
108 }),
109 Ok(_) => Ok(()),
110 Err(err) => Err(PayloadProbeError {
111 line: err.pos.line,
112 column: err.pos.column,
113 message: format!("{:?}", err.kind),
114 }),
115 }
116}
117
118fn check_libcurl() -> DoctorResult {
121 let info = hurl::http::libcurl_version_info();
122 if info.libraries.is_empty() {
123 return DoctorResult::warn("libcurl loaded but reported no libraries".to_owned());
124 }
125 DoctorResult::pass(format!("{} (host {})", info.libraries.join(" "), info.host))
126}
127
128#[cfg(test)]
129mod tests {
130 #![allow(clippy::unwrap_used)]
131
132 use super::*;
133
134 #[test]
137 fn parser_seam_smoke() {
138 let result = check_parser();
139 assert_eq!(
140 result.status,
141 proef_core::engine::DoctorStatus::Pass,
142 "{}",
143 result.detail
144 );
145 }
146
147 #[test]
148 fn factory_claims_the_hurl_step_kind() {
149 let factory = HurlEngineFactory;
150 assert_eq!(factory.id(), "hurl");
151 assert_eq!(factory.step_kinds().len(), 1);
152 assert_eq!(factory.step_kinds()[0].prefix, "hurl");
153 }
154
155 #[test]
156 fn payload_probe_accepts_valid_and_rejects_broken_hurl() {
157 assert!(validate_payload("GET http://x/one\nHTTP 200").is_ok());
158 let err = validate_payload("GET http://x/one\nHTTP 200\n[Wrong]\n").unwrap_err();
160 assert!(
161 err.line >= 2,
162 "position should be near the broken section: {err:?}"
163 );
164 }
165}