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