testing_language_server/
util.rs1use 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 assert_eq!(
119 resolve_path(&base_dir, "github.com/hoge/fuga"),
120 PathBuf::from("/Users/test/projects/github.com/hoge/fuga")
121 );
122
123 assert_eq!(
125 resolve_path(&base_dir, "./github.com/hoge/fuga"),
126 PathBuf::from("/Users/test/projects/github.com/hoge/fuga")
127 );
128
129 assert_eq!(
131 resolve_path(&base_dir, "../other/project"),
132 PathBuf::from("/Users/test/other/project")
133 );
134
135 assert_eq!(
137 resolve_path(&base_dir, "foo/bar/../../../baz"),
138 PathBuf::from("/Users/test/baz")
139 );
140
141 assert_eq!(
143 resolve_path(&base_dir, "/absolute/path"),
144 PathBuf::from("/absolute/path")
145 );
146
147 assert_eq!(
149 resolve_path(&base_dir, ""),
150 PathBuf::from("/Users/test/projects")
151 );
152
153 assert_eq!(
155 resolve_path(&base_dir, "github.com/hoge/fuga/"),
156 PathBuf::from("/Users/test/projects/github.com/hoge/fuga")
157 );
158
159 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 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 clean_old_logs(log_dir.to_str().unwrap(), 30, "prefix.log.*", "prefix.log.").unwrap();
182
183 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}