allure_rust_commons/
writer.rs1use std::{
2 collections::HashMap,
3 ffi::OsStr,
4 fs,
5 path::{Path, PathBuf},
6};
7
8use crate::model::{Categories, Globals, TestResult, TestResultContainer};
9
10#[derive(Debug, Clone)]
11pub struct FileSystemResultsWriter {
12 out_dir: PathBuf,
13}
14
15impl FileSystemResultsWriter {
16 pub fn new<P: AsRef<Path>>(out_dir: P) -> std::io::Result<Self> {
17 fs::create_dir_all(&out_dir)?;
18 Ok(Self {
19 out_dir: out_dir.as_ref().to_path_buf(),
20 })
21 }
22
23 pub fn write_result(&self, result: &TestResult) -> std::io::Result<PathBuf> {
24 self.write_result_typed(result)
25 }
26
27 pub fn write_result_typed(&self, result: &TestResult) -> std::io::Result<PathBuf> {
28 let path = self.out_dir.join(format!("{}-result.json", result.uuid));
29 self.write_json(&path, result)?;
30 Ok(path)
31 }
32
33 pub fn write_container(&self, container: &TestResultContainer) -> std::io::Result<PathBuf> {
34 self.write_container_typed(container)
35 }
36
37 pub fn write_container_typed(
38 &self,
39 container: &TestResultContainer,
40 ) -> std::io::Result<PathBuf> {
41 let path = self
42 .out_dir
43 .join(format!("{}-container.json", container.uuid));
44 self.write_json(&path, container)?;
45 Ok(path)
46 }
47
48 pub fn write_globals(&self, globals: &Globals) -> std::io::Result<PathBuf> {
49 self.write_globals_typed(globals)
50 }
51
52 pub fn write_globals_typed(&self, globals: &Globals) -> std::io::Result<PathBuf> {
53 let path = self
54 .out_dir
55 .join(format!("{}-globals.json", uuid_like_name()));
56 self.write_json(&path, globals)?;
57 Ok(path)
58 }
59
60 pub fn write_environment_properties(
61 &self,
62 properties: &HashMap<String, String>,
63 ) -> std::io::Result<PathBuf> {
64 let path = self.out_dir.join("environment.properties");
65 let mut keys = properties.keys().collect::<Vec<_>>();
66 keys.sort_unstable();
67 let content = keys
68 .into_iter()
69 .map(|k| format!("{}={}", k, &properties[k]))
70 .collect::<Vec<_>>()
71 .join("\n");
72 fs::write(&path, content)?;
73 Ok(path)
74 }
75
76 pub fn write_categories(&self, categories: &Categories) -> std::io::Result<PathBuf> {
77 self.write_categories_typed(categories)
78 }
79
80 pub fn write_categories_typed(&self, categories: &Categories) -> std::io::Result<PathBuf> {
81 let path = self.out_dir.join("categories.json");
82 self.write_json(&path, categories)?;
83 Ok(path)
84 }
85
86 pub fn write_attachment(&self, source_name: &str, bytes: &[u8]) -> std::io::Result<PathBuf> {
87 self.write_attachment_named(source_name, bytes)
88 }
89
90 pub fn write_attachment_named(
91 &self,
92 source_name: &str,
93 bytes: &[u8],
94 ) -> std::io::Result<PathBuf> {
95 let path = self.out_dir.join(source_name);
96 fs::write(&path, bytes)?;
97 Ok(path)
98 }
99
100 pub fn write_attachment_auto(
101 &self,
102 uuid: &str,
103 attachment_name: Option<&str>,
104 content_type: Option<&str>,
105 bytes: &[u8],
106 ) -> std::io::Result<(String, PathBuf)> {
107 let source_name = attachment_source_name(uuid, attachment_name, content_type);
108 let path = self.out_dir.join(&source_name);
109 fs::write(&path, bytes)?;
110 Ok((source_name, path))
111 }
112
113 fn write_json<T: serde::Serialize>(&self, path: &Path, value: &T) -> std::io::Result<()> {
114 let json = serde_json::to_vec(value)
115 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
116 fs::write(path, json)
117 }
118}
119
120pub(crate) fn attachment_source_name(
121 uuid: &str,
122 attachment_name: Option<&str>,
123 content_type: Option<&str>,
124) -> String {
125 let ext = resolve_attachment_extension(attachment_name, content_type);
126 format!("{}-attachment{}", uuid, ext)
127}
128
129fn resolve_attachment_extension(
130 attachment_name: Option<&str>,
131 content_type: Option<&str>,
132) -> String {
133 if let Some(ext) = extension_from_name(attachment_name) {
134 return ext;
135 }
136 if let Some(ext) = extension_from_content_type(content_type) {
137 return ext;
138 }
139 String::new()
140}
141
142fn extension_from_name(name: Option<&str>) -> Option<String> {
143 let name = name?;
144 let ext = Path::new(name).extension().and_then(OsStr::to_str)?;
145 if ext.is_empty() {
146 None
147 } else {
148 Some(format!(".{ext}"))
149 }
150}
151
152fn extension_from_content_type(content_type: Option<&str>) -> Option<String> {
153 let ct = content_type?.split(';').next()?.trim();
154 let ext = match ct {
155 "text/plain" => ".txt",
156 "text/html" => ".html",
157 "text/csv" => ".csv",
158 "text/xml" => ".xml",
159 "application/json" => ".json",
160 "application/xml" => ".xml",
161 "application/yaml" | "application/x-yaml" | "text/yaml" => ".yaml",
162 "image/png" => ".png",
163 "image/jpeg" => ".jpg",
164 "image/gif" => ".gif",
165 "image/svg+xml" => ".svg",
166 "video/mp4" => ".mp4",
167 _ => return None,
168 };
169 Some(ext.to_string())
170}
171
172fn uuid_like_name() -> String {
173 use std::sync::atomic::{AtomicU64, Ordering};
174 use std::time::{SystemTime, UNIX_EPOCH};
175 static COUNTER: AtomicU64 = AtomicU64::new(1);
176
177 let now = SystemTime::now()
178 .duration_since(UNIX_EPOCH)
179 .map(|d| d.as_millis())
180 .unwrap_or_default();
181 format!("{}-{}", now, COUNTER.fetch_add(1, Ordering::Relaxed))
182}
183
184#[cfg(test)]
185#[path = "writer_tests.rs"]
186mod writer_tests;