Skip to main content

allure_rust_commons/
writer.rs

1use 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;