testing_language_server/
util.rs

1use crate::error::LSError;
2use chrono::NaiveDate;
3use chrono::Utc;
4use serde::Deserialize;
5use serde::Serialize;
6use serde_json::json;
7use serde_json::Number;
8use serde_json::Value;
9use std::fs;
10use std::io::stdout;
11use std::io::Write;
12use std::path::Path;
13use std::path::PathBuf;
14
15pub fn send_stdout<T>(message: &T) -> Result<(), LSError>
16where
17    T: ?Sized + Serialize + std::fmt::Debug,
18{
19    tracing::info!("send stdout: {:#?}", message);
20    let msg = serde_json::to_string(message)?;
21    let mut stdout = stdout().lock();
22    write!(stdout, "Content-Length: {}\r\n\r\n{}", msg.len(), msg)?;
23    stdout.flush()?;
24    Ok(())
25}
26
27#[derive(Debug, Serialize, Deserialize)]
28pub struct ErrorMessage {
29    jsonrpc: String,
30    id: Option<Number>,
31    pub error: Value,
32}
33
34impl ErrorMessage {
35    #[allow(dead_code)]
36    pub fn new<N: Into<Number>>(id: Option<N>, error: Value) -> Self {
37        Self {
38            jsonrpc: "2.0".into(),
39            id: id.map(|i| i.into()),
40            error,
41        }
42    }
43}
44
45pub fn send_error<S: Into<String>>(id: Option<i64>, code: i64, msg: S) -> Result<(), LSError> {
46    send_stdout(&ErrorMessage::new(
47        id,
48        json!({ "code": code, "message": msg.into() }),
49    ))
50}
51
52pub fn format_uri(uri: &str) -> String {
53    uri.replace("file://", "")
54}
55
56pub fn resolve_path(base_dir: &Path, relative_path: &str) -> PathBuf {
57    let absolute = if Path::new(relative_path).is_absolute() {
58        PathBuf::from(relative_path)
59    } else {
60        base_dir.join(relative_path)
61    };
62
63    let mut components = Vec::new();
64    for component in absolute.components() {
65        match component {
66            std::path::Component::ParentDir => {
67                components.pop();
68            }
69            std::path::Component::Normal(_) | std::path::Component::RootDir => {
70                components.push(component);
71            }
72            _ => {}
73        }
74    }
75
76    PathBuf::from_iter(components)
77}
78
79pub fn clean_old_logs(
80    log_dir: &str,
81    retention_days: i64,
82    glob_pattern: &str,
83    prefix: &str,
84) -> Result<(), LSError> {
85    let today = Utc::now().date_naive();
86    let retention_threshold = today - chrono::Duration::days(retention_days);
87
88    let walker = globwalk::GlobWalkerBuilder::from_patterns(log_dir, &[glob_pattern])
89        .build()
90        .unwrap();
91
92    for entry in walker.filter_map(Result::ok) {
93        let path = entry.path();
94        if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) {
95            if let Some(date_str) = file_name.strip_prefix(prefix) {
96                if let Ok(file_date) = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
97                    if file_date < retention_threshold {
98                        fs::remove_file(path)?;
99                    }
100                }
101            }
102        }
103    }
104
105    Ok(())
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use std::fs::File;
112
113    #[test]
114    fn test_resolve_path() {
115        let base_dir = PathBuf::from("/Users/test/projects");
116
117        // relative path
118        assert_eq!(
119            resolve_path(&base_dir, "github.com/hoge/fuga"),
120            PathBuf::from("/Users/test/projects/github.com/hoge/fuga")
121        );
122
123        // current directory
124        assert_eq!(
125            resolve_path(&base_dir, "./github.com/hoge/fuga"),
126            PathBuf::from("/Users/test/projects/github.com/hoge/fuga")
127        );
128
129        // parent directory
130        assert_eq!(
131            resolve_path(&base_dir, "../other/project"),
132            PathBuf::from("/Users/test/other/project")
133        );
134
135        // multiple ..
136        assert_eq!(
137            resolve_path(&base_dir, "foo/bar/../../../baz"),
138            PathBuf::from("/Users/test/baz")
139        );
140
141        // absolute path
142        assert_eq!(
143            resolve_path(&base_dir, "/absolute/path"),
144            PathBuf::from("/absolute/path")
145        );
146
147        // empty relative path
148        assert_eq!(
149            resolve_path(&base_dir, ""),
150            PathBuf::from("/Users/test/projects")
151        );
152
153        // ending /
154        assert_eq!(
155            resolve_path(&base_dir, "github.com/hoge/fuga/"),
156            PathBuf::from("/Users/test/projects/github.com/hoge/fuga")
157        );
158
159        // complex path
160        assert_eq!(
161            resolve_path(&base_dir, "./foo/../bar/./baz/../qux/"),
162            PathBuf::from("/Users/test/projects/bar/qux")
163        );
164    }
165
166    #[test]
167    fn test_clean_old_logs() {
168        let home_dir = dirs::home_dir().unwrap();
169        let log_dir = home_dir.join(".config/testing_language_server/logs");
170        std::fs::create_dir_all(&log_dir).unwrap();
171
172        // Create test log files
173        let old_file = log_dir.join("prefix.log.2023-01-01");
174        File::create(&old_file).unwrap();
175        let recent_file = log_dir.join("prefix.log.2099-12-31");
176        File::create(&recent_file).unwrap();
177        let non_log_file = log_dir.join("not_a_log.txt");
178        File::create(&non_log_file).unwrap();
179
180        // Run the clean_old_logs function
181        clean_old_logs(log_dir.to_str().unwrap(), 30, "prefix.log.*", "prefix.log.").unwrap();
182
183        // Check results
184        assert!(!old_file.exists(), "Old log file should be deleted");
185        assert!(
186            recent_file.exists(),
187            "Recent log file should not be deleted"
188        );
189        assert!(non_log_file.exists(), "Non-log file should not be deleted");
190    }
191}