1use std::fs;
4use std::path::Path;
5
6use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8use yaml_rust_davvid::YamlEmitter;
9
10pub fn to_yaml<T: Serialize>(data: &T) -> Result<String> {
12 use tracing::debug;
13
14 debug!("Starting YAML serialization with hybrid approach");
15
16 let serde_value = serde_yaml::to_value(data).context("Failed to serialize to serde value")?;
18 debug!("Converted to serde_yaml::Value successfully");
19
20 let yaml_rust_value = convert_serde_to_yaml_rust(&serde_value)?;
21 debug!("Converted to yaml-rust format successfully");
22
23 let mut output = String::new();
25 let mut emitter = YamlEmitter::new(&mut output);
26 emitter.multiline_strings(true);
27 debug!("Created YamlEmitter with multiline_strings(true)");
28
29 emitter
30 .dump(&yaml_rust_value)
31 .context("Failed to emit YAML")?;
32
33 debug!(
34 output_length = output.len(),
35 output_preview = %output.lines().take(10).collect::<Vec<_>>().join("\\n"),
36 "YAML serialization completed"
37 );
38
39 Ok(output)
40}
41
42fn convert_serde_to_yaml_rust(value: &serde_yaml::Value) -> Result<yaml_rust_davvid::Yaml> {
44 use tracing::debug;
45 use yaml_rust_davvid::Yaml;
46
47 match value {
48 serde_yaml::Value::Null => Ok(Yaml::Null),
49 serde_yaml::Value::Bool(b) => Ok(Yaml::Boolean(*b)),
50 serde_yaml::Value::Number(n) => {
51 if let Some(i) = n.as_i64() {
52 Ok(Yaml::Integer(i))
53 } else if let Some(f) = n.as_f64() {
54 Ok(Yaml::Real(f.to_string()))
55 } else {
56 Ok(Yaml::String(n.to_string()))
57 }
58 }
59 serde_yaml::Value::String(s) => {
60 debug!(
61 string_length = s.len(),
62 string_preview = %s.lines().take(3).collect::<Vec<_>>().join("\\n"),
63 "Converting string value to yaml-rust"
64 );
65 Ok(Yaml::String(s.clone()))
67 }
68 serde_yaml::Value::Sequence(seq) => {
69 let yaml_seq: Result<Vec<_>> = seq.iter().map(convert_serde_to_yaml_rust).collect();
70 Ok(Yaml::Array(yaml_seq?))
71 }
72 serde_yaml::Value::Mapping(map) => {
73 let mut yaml_map = yaml_rust_davvid::yaml::Hash::new();
74 for (k, v) in map {
75 let yaml_key = convert_serde_to_yaml_rust(k)?;
76 let yaml_value = convert_serde_to_yaml_rust(v)?;
77 yaml_map.insert(yaml_key, yaml_value);
78 }
79 Ok(Yaml::Hash(yaml_map))
80 }
81 serde_yaml::Value::Tagged(tagged) => {
82 convert_serde_to_yaml_rust(&tagged.value)
84 }
85 }
86}
87
88pub fn from_yaml<T: for<'de> Deserialize<'de>>(yaml: &str) -> Result<T> {
90 use tracing::debug;
91
92 debug!(
93 yaml_length = yaml.len(),
94 yaml_preview = %yaml.lines().take(10).collect::<Vec<_>>().join("\\n"),
95 "Deserializing YAML using serde_yaml"
96 );
97
98 let result = serde_yaml::from_str(yaml).context("Failed to deserialize YAML");
99
100 debug!(
101 success = result.is_ok(),
102 error = result
103 .as_ref()
104 .err()
105 .map(|e| e.to_string())
106 .unwrap_or_default(),
107 "YAML deserialization result"
108 );
109
110 result
111}
112
113pub fn read_yaml_file<T: for<'de> Deserialize<'de>, P: AsRef<Path>>(path: P) -> Result<T> {
115 let content = fs::read_to_string(&path)
116 .with_context(|| format!("Failed to read file: {}", path.as_ref().display()))?;
117
118 from_yaml(&content)
119}
120
121pub fn write_yaml_file<T: Serialize, P: AsRef<Path>>(data: &T, path: P) -> Result<()> {
123 let yaml_content = to_yaml(data)?;
124
125 fs::write(&path, yaml_content)
126 .with_context(|| format!("Failed to write file: {}", path.as_ref().display()))?;
127
128 Ok(())
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use serde::{Deserialize, Serialize};
135
136 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137 struct TestDiffContent {
138 diff_content: String,
139 description: String,
140 }
141
142 #[test]
143 fn multiline_yaml_with_literal_blocks() {
144 let test_data = TestDiffContent {
145 diff_content: "diff --git a/file.txt b/file.txt\nindex 123..456 100644\n--- a/file.txt\n+++ b/file.txt\n@@ -1,3 +1,3 @@\n-old line\n+new line".to_string(),
146 description: "This is a\nmultiline\ndescription".to_string(),
147 };
148
149 let yaml_output = to_yaml(&test_data).unwrap();
150 println!("YAML Output:\n{}", yaml_output);
151
152 assert!(yaml_output.contains("diff_content: |"));
154 assert!(yaml_output.contains("description: |"));
155
156 assert!(yaml_output.contains("diff --git"));
158 assert!(yaml_output.contains("--- a/file.txt"));
159 assert!(yaml_output.contains("+++ b/file.txt"));
160
161 assert!(!yaml_output.contains("\\n"));
163
164 let deserialized: TestDiffContent = from_yaml(&yaml_output).unwrap();
166
167 assert_eq!(test_data.description, deserialized.description);
169
170 assert!(
172 deserialized.diff_content == test_data.diff_content
173 || deserialized.diff_content == format!("{}\n", test_data.diff_content)
174 );
175 }
176
177 #[test]
178 fn yaml_round_trip_preserves_content() {
179 let original = TestDiffContent {
180 diff_content: "line1\nline2\nline3".to_string(),
181 description: "desc line1\ndesc line2".to_string(),
182 };
183
184 let yaml_output = to_yaml(&original).unwrap();
185 let deserialized: TestDiffContent = from_yaml(&yaml_output).unwrap();
186
187 assert_eq!(original.description, deserialized.description);
189 assert!(
190 deserialized.diff_content == original.diff_content
191 || deserialized.diff_content == format!("{}\n", original.diff_content)
192 );
193 }
194
195 #[test]
196 fn ai_response_like_yaml_parsing() {
197 let ai_response_yaml = r#"title: "deps(test): upgrade hedgehog-extras to 0.10.0.0"
199description: |
200 # Changelog
201
202 ```yaml
203 - description: |
204 Upgrade hedgehog-extras dependency from 0.7.1+ to ^>=0.10.0.0 to access newer testing utilities and improvements. Updated type constraints and imports to maintain compatibility with the enhanced testing framework.
205 type:
206 - test # fixes/modifies tests
207 - maintenance # not directly related to the code
208 ```
209
210 # Context
211
212 This PR upgrades the `hedgehog-extras` testing library from version 0.7.1+ to 0.10.0.0 to leverage newer testing utilities and improvements. The upgrade requires several compatibility changes to maintain existing test functionality while accessing the enhanced testing framework capabilities.
213
214 The changes ensure that the Cardano CLI test suite continues to work correctly with the updated dependency while taking advantage of improvements in the newer version of hedgehog-extras.
215
216 # How to trust this PR
217
218 **Key areas to review:**
219
220 1. **Dependency constraint update** in `cardano-cli/cardano-cli.cabal` - verify the version constraint change from `>=0.7.1` to `^>=0.10`
221
222 2. **Type signature enhancement** in `Test/Cli/Run/Hash.hs` - the `hash_trip_fun` function now includes additional type constraints (`MonadBaseControl IO m` and `H.MonadAssertion m`) required by hedgehog-extras 0.10
223
224 3. **Import additions** - new imports for `FlexibleContexts` language extension and `MonadBaseControl` to support the updated API
225
226 **Commands to verify the changes:**
227 ```bash
228 # Verify the project builds with new dependencies
229 cabal build cardano-cli-test-lib
230
231 # Run the hash tests specifically
232 cabal test cardano-cli-test --test-options="--pattern Hash"
233
234 # Check that all tests still pass
235 cabal test cardano-cli-test
236 ```
237
238 **Specific changes made:**
239
240 - **cabal.project**: Updated Hackage index-state from 2025-06-22 to 2025-09-10 for latest package availability
241 - **cardano-cli.cabal**: Changed hedgehog-extras constraint from `>=0.7.1` to `^>=0.10`
242 - **Test/Cli/Run/Hash.hs**:
243 - Added `FlexibleContexts` language extension
244 - Imported `MonadBaseControl` from `Control.Monad.Trans.Control`
245 - Extended `hash_trip_fun` type signature with `MonadBaseControl IO m` and `H.MonadAssertion m` constraints
246 - **flake.lock**: Updated dependency hashes to reflect the new package versions
247
248 The type constraint additions are necessary because hedgehog-extras 0.10 has enhanced its monad transformer support, requiring these additional capabilities for proper test execution.
249
250 # Checklist
251
252 - [x] Commit sequence broadly makes sense and commits have useful messages
253 - [x] New tests are added if needed and existing tests are updated. See [Running tests](https://github.com/input-output-hk/cardano-node-wiki/wiki/Running-tests) for more details
254 - [x] Self-reviewed the diff"#;
255
256 #[derive(serde::Deserialize)]
258 struct PrContent {
259 title: String,
260 description: String,
261 }
262
263 println!("Testing YAML parsing with AI response...");
264 println!("Input length: {} chars", ai_response_yaml.len());
265 println!(
266 "First 200 chars: {}",
267 &ai_response_yaml[..200.min(ai_response_yaml.len())]
268 );
269
270 let pr_content: PrContent = from_yaml(ai_response_yaml).unwrap();
271
272 println!("Parsed title: {}", pr_content.title);
273 println!(
274 "Parsed description length: {}",
275 pr_content.description.len()
276 );
277 println!("Description first 3 lines:");
278 for (i, line) in pr_content.description.lines().take(3).enumerate() {
279 println!(" {}: {}", i + 1, line);
280 }
281
282 assert_eq!(
283 pr_content.title,
284 "deps(test): upgrade hedgehog-extras to 0.10.0.0"
285 );
286 assert!(pr_content.description.contains("# Changelog"));
287 assert!(pr_content.description.contains("# How to trust this PR"));
288 assert!(pr_content.description.contains("**Key areas to review:**"));
289 assert!(pr_content.description.contains("# Checklist"));
290
291 let lines: Vec<&str> = pr_content.description.lines().collect();
293 assert!(
294 lines.len() > 20,
295 "Should have many lines, got {}",
296 lines.len()
297 );
298
299 assert!(
301 pr_content.description.len() > 100,
302 "Description should be long, got {}",
303 pr_content.description.len()
304 );
305 }
306}