1use anyhow::{Context, Result};
12use regex::Regex;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::fs;
16use std::path::PathBuf;
17use tera::Tera;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PolicyVariant {
22 pub name: String,
23 pub values: HashMap<String, serde_yaml::Value>,
24 pub description: Option<String>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct QosPolicy {
30 pub id: usize,
31 pub name: String,
32 pub category: String,
33 pub description: Option<String>,
34 pub xml_path: Vec<String>,
35 pub scope: Vec<String>,
36 pub variants: Vec<PolicyVariant>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct QosValidatorConfig {
42 pub policies: Vec<QosPolicy>,
43 pub generator: GeneratorConfig,
44 pub baseline_config: BaselineConfig,
45 pub test_config: TestConfig,
46 pub metadata: Metadata,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct GeneratorConfig {
51 pub output_dirs: HashMap<String, String>,
52 pub baseline_template: String,
53 pub test_script_template: String,
54 pub shared_utilities: Vec<String>,
55 pub manifest_output: String,
56 pub agent_snippet_output: String,
57 pub makefile_snippet_output: String,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct BaselineConfig {
62 pub transport: String,
63 pub topic_name: String,
64 pub data_type: String,
65 pub defaults: HashMap<String, serde_yaml::Value>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct TestConfig {
70 pub subscriber_timeout: u32,
71 pub publisher_timeout: u32,
72 pub discovery_delay: u32,
73 pub network: HashMap<String, String>,
74 pub remote: HashMap<String, String>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct Metadata {
79 pub version: String,
80 pub created_date: String,
81 pub description: String,
82}
83
84pub struct QosGenerator {
86 config: QosValidatorConfig,
87 base_dir: PathBuf,
88 tera: Tera,
89}
90
91impl QosGenerator {
92 pub fn new(base_dir: PathBuf) -> Result<Self> {
94 let config_path = base_dir.join("interop/validator/generator/config.yaml");
95
96 tracing::info!("Loading config from: {:?}", config_path);
97 let config_content =
98 fs::read_to_string(&config_path).context("Failed to read config.yaml")?;
99 let config: QosValidatorConfig =
100 serde_yaml::from_str(&config_content).context("Failed to parse config.yaml")?;
101
102 let mut tera = Tera::default();
104
105 let baseline_path = base_dir.join(&config.generator.baseline_template);
107 tracing::info!("Loading baseline template: {:?}", baseline_path);
108 let baseline_content =
109 fs::read_to_string(&baseline_path).context("Failed to read baseline.xml")?;
110 tera.add_raw_template("baseline", &baseline_content)
111 .context("Failed to parse baseline.xml template")?;
112
113 let test_template_path = base_dir.join(&config.generator.test_script_template);
115 tracing::info!("Loading test script template: {:?}", test_template_path);
116 let test_template_content =
117 fs::read_to_string(&test_template_path).context("Failed to read test_script.sh.j2")?;
118 tera.add_raw_template("test_script", &test_template_content)
119 .context("Failed to parse test_script.sh.j2 template")?;
120
121 Ok(Self {
122 config,
123 base_dir,
124 tera,
125 })
126 }
127
128 pub fn generate(&self) -> Result<GenerationReport> {
130 tracing::info!("Starting QoS validator generation");
131
132 let mut report = GenerationReport::new();
133
134 tracing::info!("Stage 1: Generating QoS XML profiles");
136 self.generate_qos_profiles(&mut report)?;
137
138 tracing::info!("Stage 2: Generating test scripts");
140 self.generate_test_scripts(&mut report)?;
141
142 tracing::info!("Stage 3: Generating manifest");
144 self.generate_manifest(&report)?;
145
146 tracing::info!("Stage 4: Generating integration snippets");
148 self.generate_agent_snippet(&report)?;
149 self.generate_makefile_snippet(&report)?;
150
151 tracing::info!("[OK] Generation complete");
152 Ok(report)
153 }
154
155 fn generate_qos_profiles(&self, report: &mut GenerationReport) -> Result<()> {
157 let output_dir = self.base_dir.join("interop/validator/qos");
158 fs::create_dir_all(&output_dir).context("Failed to create qos directory")?;
159
160 for policy in &self.config.policies {
161 for variant in &policy.variants {
162 let filename =
163 format!("policy_{:03}_{}_{}", policy.id, policy.name, variant.name) + ".xml";
164 let filepath = output_dir.join(&filename);
165
166 let mut ctx = tera::Context::new();
168 ctx.insert("policy_id", &format!("{:03}", policy.id));
169 ctx.insert("policy_name", &policy.name);
170 ctx.insert("variant_name", &variant.name);
171
172 let mut rendered = self
174 .tera
175 .render("baseline", &ctx)
176 .context(format!("Failed to render baseline for {}", policy.name))?;
177
178 rendered = self
180 .apply_variant_overrides(&rendered, policy, variant)
181 .context(format!(
182 "Failed to apply overrides for policy {}",
183 policy.id
184 ))?;
185
186 fs::write(&filepath, &rendered).context(format!("Failed to write {}", filename))?;
187
188 report.qos_files_generated.push(filename);
189 }
190 }
191
192 tracing::info!(
193 "[OK] Generated {} QoS profiles",
194 report.qos_files_generated.len()
195 );
196 Ok(())
197 }
198
199 fn apply_variant_overrides(
201 &self,
202 baseline_xml: &str,
203 _policy: &QosPolicy,
204 variant: &PolicyVariant,
205 ) -> Result<String> {
206 let mut result = baseline_xml.to_string();
207
208 for (xml_path, override_value) in &variant.values {
211 let value_str = match override_value {
213 serde_yaml::Value::String(s) => s.clone(),
214 serde_yaml::Value::Number(n) => n.to_string(),
215 serde_yaml::Value::Bool(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
216 _ => continue,
217 };
218
219 let parts: Vec<&str> = xml_path.split('/').collect();
221 if parts.len() < 2 {
222 continue; }
224
225 let parent_elem = parts[parts.len() - 2];
226 let child_elem = parts[parts.len() - 1];
227
228 let pattern = format!(
231 "<{}>[\\s\\S]*?<{}>[^<]*</{}>",
232 regex::escape(parent_elem),
233 regex::escape(child_elem),
234 regex::escape(child_elem)
235 );
236
237 if let Ok(re) = Regex::new(&pattern) {
238 let replacement = format!(
240 "<{}><{}>{}</{}>",
241 parent_elem, child_elem, value_str, child_elem
242 );
243
244 result = re.replace_all(&result, &replacement).to_string();
245 }
246 }
247
248 Ok(result)
249 }
250
251 fn generate_test_scripts(&self, report: &mut GenerationReport) -> Result<()> {
253 let output_dir = self.base_dir.join("interop/validator/scripts");
254 fs::create_dir_all(&output_dir).context("Failed to create scripts directory")?;
255
256 let directions = vec![("fd2hd", "FastDDS to HDDS"), ("hd2fd", "HDDS to FastDDS")];
257
258 for policy in &self.config.policies {
259 for variant in &policy.variants {
260 for (dir, dir_label) in &directions {
261 let filename = format!(
262 "policy_{:03}_{}_{}_{}",
263 policy.id, policy.name, variant.name, dir
264 ) + ".sh";
265 let filepath = output_dir.join(&filename);
266
267 let qos_filename = format!(
268 "policy_{:03}_{}_{}.xml",
269 policy.id, policy.name, variant.name
270 );
271
272 let mut ctx = tera::Context::new();
274 ctx.insert("policy_id", &format!("{:03}", policy.id));
275 ctx.insert("policy_name", &policy.name);
276 ctx.insert("policy_category", &policy.category);
277 ctx.insert("variant_name", &variant.name);
278 ctx.insert(
279 "variant_description",
280 &variant.description.as_ref().unwrap_or(&"".to_string()),
281 );
282 ctx.insert("direction", dir);
283 ctx.insert("direction_label", dir_label);
284 ctx.insert(
285 "profile_name",
286 &format!("policy_{:03}_{}_{}", policy.id, policy.name, variant.name),
287 );
288 ctx.insert("qos_file", &qos_filename);
289
290 let rendered = match self.tera.render("test_script", &ctx) {
291 Ok(r) => r,
292 Err(e) => {
293 eprintln!("Tera error: {}", e);
294 eprintln!("Context: policy_id={}, direction={}", policy.id, dir);
295 Err(e).context(format!(
296 "Failed to render test script for policy {} variant {}",
297 policy.id, variant.name
298 ))?
299 }
300 };
301
302 fs::write(&filepath, &rendered)
303 .context(format!("Failed to write {}", filename))?;
304
305 #[cfg(unix)]
307 {
308 use std::os::unix::fs::PermissionsExt;
309 let perms = fs::Permissions::from_mode(0o755);
310 fs::set_permissions(&filepath, perms)
311 .context(format!("Failed to chmod {}", filename))?;
312 }
313
314 report.test_scripts_generated.push(filename);
315 }
316 }
317 }
318
319 tracing::info!(
320 "[OK] Generated {} test scripts",
321 report.test_scripts_generated.len()
322 );
323 Ok(())
324 }
325
326 fn generate_manifest(&self, report: &GenerationReport) -> Result<()> {
328 let output_dir = self.base_dir.join("interop/validator/generator/output");
329 fs::create_dir_all(&output_dir).context("Failed to create output directory")?;
330
331 let manifest = serde_json::json!({
332 "generated_date": chrono::Local::now().to_rfc3339(),
333 "qos_profiles": report.qos_files_generated,
334 "test_scripts": report.test_scripts_generated,
335 "total_tests": report.test_scripts_generated.len(),
336 "policies_count": self.config.policies.len(),
337 "variants_count": report.qos_files_generated.len(),
338 });
339
340 let manifest_path = output_dir.join("manifest.json");
341 fs::write(&manifest_path, serde_json::to_string_pretty(&manifest)?)
342 .context("Failed to write manifest.json")?;
343
344 tracing::info!("[OK] Generated manifest.json");
345 Ok(())
346 }
347
348 fn generate_agent_snippet(&self, _report: &GenerationReport) -> Result<()> {
350 let output_dir = self.base_dir.join("interop/validator/generator/output");
351 fs::create_dir_all(&output_dir).context("Failed to create output directory")?;
352
353 let mut snippet =
354 String::from("# Auto-generated agent cases (append to agent_watch.sh)\n\n");
355
356 let directions = vec!["fd2hd", "hd2fd"];
357 for policy in &self.config.policies {
358 for variant in &policy.variants {
359 for dir in &directions {
360 let task_name = format!(
361 "validator_policy_{:03}_{}_{}_{}",
362 policy.id, policy.name, variant.name, dir
363 );
364 let script_name = format!(
365 "policy_{:03}_{}_{}_{}.sh",
366 policy.id, policy.name, variant.name, dir
367 );
368
369 snippet.push_str(&format!(
370 " {}) \n bash {}\n ;;\n\n",
371 task_name, script_name
372 ));
373 }
374 }
375 }
376
377 let snippet_path = output_dir.join("agent_watch_snippet.sh");
378 fs::write(&snippet_path, snippet).context("Failed to write agent_watch_snippet.sh")?;
379
380 tracing::info!("[OK] Generated agent_watch_snippet.sh");
381 Ok(())
382 }
383
384 fn generate_makefile_snippet(&self, _report: &GenerationReport) -> Result<()> {
386 let output_dir = self.base_dir.join("interop/validator/generator/output");
387 fs::create_dir_all(&output_dir).context("Failed to create output directory")?;
388
389 let mut snippet =
390 String::from("# Auto-generated Makefile targets (append to Makefile)\n\n");
391
392 let directions = vec!["fd2hd", "hd2fd"];
393 for policy in &self.config.policies {
394 for variant in &policy.variants {
395 for dir in &directions {
396 let target_name = format!(
397 "run-validator-policy-{:03}-{}-{}-{}",
398 policy.id,
399 policy.name.replace("_", "-"),
400 variant.name.replace("_", "-"),
401 dir
402 );
403 let task_name = format!(
404 "validator_policy_{:03}_{}_{}_{}",
405 policy.id, policy.name, variant.name, dir
406 );
407
408 snippet.push_str(&format!(
409 "{}:\n\t@printf 'TASK={}\\n' > agent_triggers/run\n\n",
410 target_name, task_name
411 ));
412 }
413 }
414 }
415
416 let snippet_path = output_dir.join("makefile_snippet.mk");
417 fs::write(&snippet_path, snippet).context("Failed to write makefile_snippet.mk")?;
418
419 tracing::info!("[OK] Generated makefile_snippet.mk");
420 Ok(())
421 }
422}
423
424#[derive(Default)]
426pub struct GenerationReport {
427 pub qos_files_generated: Vec<String>,
428 pub test_scripts_generated: Vec<String>,
429}
430
431impl GenerationReport {
432 pub fn new() -> Self {
433 Self::default()
434 }
435
436 pub fn summary(&self) {
437 println!("\n{}", "=".repeat(60));
438 println!(" QoS Validator Generation Report");
439 println!("{}", "=".repeat(60));
440 println!();
441 println!(
442 " [OK] QoS Profiles: {} files",
443 self.qos_files_generated.len()
444 );
445 println!(
446 " [OK] Test Scripts: {} files",
447 self.test_scripts_generated.len()
448 );
449 println!(
450 " [OK] Total Tests: {} (variants x 2 directions)",
451 self.test_scripts_generated.len() / 2
452 );
453 println!();
454 println!(" Generated in:");
455 println!(" - <base_dir>/interop/validator/qos/");
456 println!(" - <base_dir>/interop/validator/scripts/");
457 println!(" - <base_dir>/interop/validator/generator/output/");
458 println!();
459 println!("{}", "=".repeat(60));
460 }
461}