,
443 | HD: FnOnce() -> Option,
444 | {
445 | let input_str = input.as_ref();
446 | let bytes = input_str.as_os_str().as_bytes();
447 | if bytes[0] == b'~' {
448 | let input_after_tilde = &bytes[1..];
449 | if input_after_tilde.is_empty() || input_after_tilde[0] == b'/' {
450 | if let Some(hd) = home_dir() {
451 | let mut s = OsString::new();
452 | s.push(hd.as_ref());
453 | s.push(OsStr::from_bytes(input_after_tilde));
454 | PathBuf::from(s).into()
455 | } else {
456 | // home dir is not available
457 | input_str.into()
458 | }
459 | } else {
460 | // we cannot handle `~otheruser/` paths yet
461 | input_str.into()
462 | }
463 | } else {
464 | // input doesn't start with tilde
465 | input_str.into()
466 | }
467 | }
468 |
469 | fn tilde(input: &SI) -> Cow
470 | where
471 | SI: AsRef,
472 | {
473 | tilde_with_context(input, dirs_next::home_dir)
474 | }
475 |
--------------------------------------------------------------------------------
/tests/projects.rs:
--------------------------------------------------------------------------------
1 | // Copyright Pit Kleyersburg
2 | //
3 | // Licensed under the Apache License, Version 2.0 or the MIT license
5 | // , at your
6 | // option. This file may not be copied, modified or distributed
7 | // except according to those terms.
8 |
9 | #![cfg(feature = "sequential-tests")]
10 |
11 | use i3nator::{
12 | configfiles::ConfigFile,
13 | projects::{self, Project},
14 | types::*,
15 | };
16 | use lazy_static::lazy_static;
17 | use std::{
18 | env,
19 | ffi::OsString,
20 | fs::{self, File},
21 | io::prelude::*,
22 | panic::{self, UnwindSafe},
23 | path::{Path, PathBuf},
24 | };
25 | use tempdir::TempDir;
26 | use tempfile::NamedTempFile;
27 |
28 | lazy_static! {
29 | static ref TMP_DIR: TempDir = TempDir::new("i3nator-tests").unwrap();
30 | static ref PROJECTS_DIR: PathBuf = TMP_DIR.path().join("i3nator/projects");
31 | }
32 |
33 | fn with_projects_dir ()>(body: F)
34 | where
35 | F: UnwindSafe,
36 | {
37 | // Create the temporary directories if they do not exist
38 | if !PROJECTS_DIR.exists() {
39 | fs::create_dir_all(&*PROJECTS_DIR).expect("couldn't create temporary directories");
40 | }
41 |
42 | // Set up temporary XDG config directory
43 | env::set_var("XDG_CONFIG_HOME", TMP_DIR.path());
44 |
45 | // Run body
46 | let panic_result = panic::catch_unwind(|| body(PROJECTS_DIR.as_ref()));
47 |
48 | // Remove the temporary directories
49 | fs::remove_dir_all(&*TMP_DIR).expect("couldn't delete temporary directories");
50 |
51 | if let Err(err) = panic_result {
52 | panic::resume_unwind(err);
53 | }
54 | }
55 |
56 | #[test]
57 | fn empty_list() {
58 | with_projects_dir(|_| {
59 | assert!(projects::list().is_empty());
60 | })
61 | }
62 |
63 | #[test]
64 | fn create() {
65 | with_projects_dir(|projects_dir| {
66 | let project = Project::create("project-one").unwrap();
67 | assert_eq!(project.name, "project-one");
68 | assert_eq!(project.path, projects_dir.join("project-one.toml"));
69 | assert!(project.verify().is_err());
70 |
71 | // File does not get created by default, list should still be empty
72 | assert!(projects::list().is_empty());
73 | })
74 | }
75 |
76 | #[test]
77 | #[should_panic(expected = "ConfigExists")]
78 | fn create_exists() {
79 | with_projects_dir(|projects_dir| {
80 | let project = Project::create("project-one").unwrap();
81 | assert_eq!(project.name, "project-one");
82 | assert_eq!(project.path, projects_dir.join("project-one.toml"));
83 | assert!(project.verify().is_err());
84 |
85 | // Create project file
86 | File::create(&project.path).expect("couldn't create project file");
87 |
88 | // File created, list should contain it
89 | assert_eq!(projects::list(), vec![OsString::from("project-one")]);
90 |
91 | // Create project with same name, this should fail
92 | Project::create("project-one").unwrap();
93 | })
94 | }
95 |
96 | #[test]
97 | fn create_from_template() {
98 | with_projects_dir(|projects_dir| {
99 | let template = "this is my template";
100 | let project =
101 | Project::create_from_template("project-template", template.as_bytes()).unwrap();
102 |
103 | assert_eq!(project.name, "project-template");
104 | assert_eq!(project.path, projects_dir.join("project-template.toml"));
105 | assert!(project.path.exists());
106 | assert!(project.verify().is_err());
107 |
108 | let mut file = File::open(project.path).unwrap();
109 | let mut contents = String::new();
110 | file.read_to_string(&mut contents).unwrap();
111 |
112 | assert_eq!(contents, template);
113 | })
114 | }
115 |
116 | #[test]
117 | fn from_path() {
118 | let tempfile = NamedTempFile::new().expect("couldn't create temporary file");
119 | let project = Project::from_path(tempfile.path()).unwrap();
120 | assert_eq!(project.name, "local");
121 | assert_eq!(project.path, tempfile.path());
122 | assert!(project.verify().is_err());
123 | }
124 |
125 | #[test]
126 | #[should_panic(expected = "PathDoesntExist")]
127 | fn from_path_not_exists() {
128 | Project::from_path("/this/path/does/not/exist").unwrap();
129 | }
130 |
131 | #[test]
132 | fn open() {
133 | with_projects_dir(|projects_dir| {
134 | let project = Project::create("project-open").unwrap();
135 | assert_eq!(project.name, "project-open");
136 | assert_eq!(project.path, projects_dir.join("project-open.toml"));
137 | assert!(project.verify().is_err());
138 |
139 | // Create project file
140 | File::create(&project.path).expect("couldn't create project file");
141 |
142 | // Open project
143 | let project_open = Project::open("project-open").unwrap();
144 | assert_eq!(project_open, project);
145 | })
146 | }
147 |
148 | #[test]
149 | #[should_panic(expected = "UnknownConfig")]
150 | fn open_unknown_project() {
151 | with_projects_dir(|_| {
152 | Project::open("unknown-project").unwrap();
153 | })
154 | }
155 |
156 | #[test]
157 | fn config() {
158 | with_projects_dir(|projects_dir| {
159 | let template = r#"[general]
160 | layout = "{ ... }"
161 |
162 | [[applications]]
163 | command = "mycommand""#;
164 | let mut project =
165 | Project::create_from_template("project-template", template.as_bytes()).unwrap();
166 |
167 | assert_eq!(project.name, "project-template");
168 | assert_eq!(project.path, projects_dir.join("project-template.toml"));
169 | assert!(project.path.exists());
170 | assert!(project.verify().is_ok());
171 |
172 | let expected = Config {
173 | general: General {
174 | working_directory: None,
175 | workspace: None,
176 | layout: Layout::Contents("{ ... }".to_owned()),
177 | },
178 | applications: vec![Application {
179 | command: ApplicationCommand {
180 | program: "mycommand".to_owned(),
181 | args: vec![],
182 | },
183 | working_directory: None,
184 | exec: None,
185 | }],
186 | };
187 |
188 | assert_eq!(project.config().unwrap(), &expected);
189 | })
190 | }
191 |
192 | #[test]
193 | fn config_invalid() {
194 | with_projects_dir(|projects_dir| {
195 | let template = r#"invalid template"#;
196 | let mut project =
197 | Project::create_from_template("project-template", template.as_bytes()).unwrap();
198 |
199 | assert_eq!(project.name, "project-template");
200 | assert_eq!(project.path, projects_dir.join("project-template.toml"));
201 | assert!(project.path.exists());
202 | assert!(project.verify().is_err());
203 | assert!(project.config().is_err());
204 | })
205 | }
206 |
207 | #[test]
208 | fn copy() {
209 | with_projects_dir(|projects_dir| {
210 | let project = Project::create("project-existing").unwrap();
211 | assert_eq!(project.name, "project-existing");
212 | assert_eq!(project.path, projects_dir.join("project-existing.toml"));
213 | assert!(project.verify().is_err());
214 |
215 | // Create project file
216 | File::create(&project.path).expect("couldn't create project file");
217 |
218 | let project_new = project.copy("project-new").unwrap();
219 | assert_eq!(project_new.name, "project-new");
220 | assert_eq!(project_new.path, projects_dir.join("project-new.toml"));
221 | assert!(project.verify().is_err());
222 | })
223 | }
224 |
225 | #[test]
226 | #[should_panic(expected = "No such file or directory")]
227 | fn copy_without_file() {
228 | with_projects_dir(|projects_dir| {
229 | let project = Project::create("project-existing").unwrap();
230 | assert_eq!(project.name, "project-existing");
231 | assert_eq!(project.path, projects_dir.join("project-existing.toml"));
232 | assert!(project.verify().is_err());
233 |
234 | project.copy("project-new").unwrap();
235 | })
236 | }
237 |
238 | #[test]
239 | fn delete() {
240 | with_projects_dir(|projects_dir| {
241 | let project = Project::create("project-delete").unwrap();
242 | assert_eq!(project.name, "project-delete");
243 | assert_eq!(project.path, projects_dir.join("project-delete.toml"));
244 | assert!(project.verify().is_err());
245 |
246 | // Create project file
247 | File::create(&project.path).expect("couldn't create project file");
248 |
249 | assert!(project.delete().is_ok());
250 | assert!(!project.path.exists())
251 | })
252 | }
253 |
254 | #[test]
255 | #[should_panic(expected = "No such file or directory")]
256 | fn delete_without_file() {
257 | with_projects_dir(|projects_dir| {
258 | let project = Project::create("project-delete").unwrap();
259 | assert_eq!(project.name, "project-delete");
260 | assert_eq!(project.path, projects_dir.join("project-delete.toml"));
261 | assert!(project.verify().is_err());
262 |
263 | project.delete().unwrap();
264 | })
265 | }
266 |
267 | #[test]
268 | fn rename() {
269 | with_projects_dir(|projects_dir| {
270 | let project = Project::create("project-rename-old").unwrap();
271 | assert_eq!(project.name, "project-rename-old");
272 | assert_eq!(project.path, projects_dir.join("project-rename-old.toml"));
273 | assert!(project.verify().is_err());
274 |
275 | // Create project file
276 | File::create(&project.path).expect("couldn't create project file");
277 |
278 | let project_new = project.rename("project-rename-new").unwrap();
279 | assert_eq!(project_new.name, "project-rename-new");
280 | assert_eq!(
281 | project_new.path,
282 | projects_dir.join("project-rename-new.toml")
283 | );
284 | assert!(project_new.verify().is_err());
285 |
286 | assert!(!project.path.exists());
287 | assert!(project_new.path.exists());
288 | })
289 | }
290 |
--------------------------------------------------------------------------------
/tests/types.rs:
--------------------------------------------------------------------------------
1 | // Copyright Pit Kleyersburg
2 | //
3 | // Licensed under the Apache License, Version 2.0 or the MIT license
5 | // , at your
6 | // option. This file may not be copied, modified or distributed
7 | // except according to those terms.
8 |
9 | use i3nator::types::*;
10 | use std::time::Duration;
11 |
12 | macro_rules! equivalent {
13 | ( $fragment:expr, $expected:expr; $ty:ty ) => {
14 | let actual: $ty = toml::from_str($fragment).unwrap();
15 | assert_eq!(actual, $expected);
16 | };
17 | }
18 |
19 | #[test]
20 | fn full_config() {
21 | let expected = Config {
22 | general: General {
23 | working_directory: Some("/path/to/my/working/directory".to_owned().into()),
24 | workspace: Some("0".to_owned()),
25 | layout: Layout::Path("/path/to/my/layout.json".into()),
26 | },
27 | applications: vec![Application {
28 | command: ApplicationCommand {
29 | program: "mycommand".to_owned(),
30 | args: vec!["--with".to_owned(), "multiple args".to_owned()],
31 | },
32 | working_directory: Some("/path/to/a/different/working/directory".to_owned().into()),
33 | exec: Some(Exec {
34 | commands: vec!["command one".to_owned(), "command two".to_owned()],
35 | exec_type: ExecType::TextNoReturn,
36 | timeout: Duration::from_secs(5),
37 | }),
38 | }],
39 | };
40 |
41 | equivalent! {
42 | r#"
43 | [general]
44 | working_directory = "/path/to/my/working/directory"
45 | workspace = "0"
46 | layout = "/path/to/my/layout.json"
47 |
48 | [[applications]]
49 | command = "mycommand --with 'multiple args'"
50 | working_directory = "/path/to/a/different/working/directory"
51 | exec = { commands = ["command one", "command two"], exec_type = "text_no_return" }
52 | "#,
53 | expected;
54 | Config
55 | }
56 | }
57 |
58 | #[test]
59 | fn application_command_str() {
60 | let expected = Application {
61 | command: ApplicationCommand {
62 | program: "mycommand".to_owned(),
63 | args: vec!["--with".to_owned(), "multiple args".to_owned()],
64 | },
65 | working_directory: None,
66 | exec: None,
67 | };
68 |
69 | equivalent! {
70 | r#"command = "mycommand --with 'multiple args'""#,
71 | expected;
72 | Application
73 | }
74 | }
75 |
76 | #[test]
77 | fn application_command_str_no_args() {
78 | let expected = Application {
79 | command: ApplicationCommand {
80 | program: "mycommand".to_owned(),
81 | args: vec![],
82 | },
83 | working_directory: None,
84 | exec: None,
85 | };
86 |
87 | equivalent! {
88 | r#"command = "mycommand""#,
89 | expected;
90 | Application
91 | }
92 | }
93 |
94 | #[test]
95 | #[should_panic(expected = "command can not be empty")]
96 | fn application_command_empty_str() {
97 | toml::from_str::(r#"command = """#).unwrap();
98 | }
99 |
100 | #[test]
101 | fn application_command_seq() {
102 | let expected = Application {
103 | command: ApplicationCommand {
104 | program: "mycommand".to_owned(),
105 | args: vec!["--with".to_owned(), "multiple args".to_owned()],
106 | },
107 | working_directory: None,
108 | exec: None,
109 | };
110 |
111 | equivalent! {
112 | r#"command = ["mycommand", "--with", "multiple args"]"#,
113 | expected;
114 | Application
115 | }
116 | }
117 |
118 | #[test]
119 | fn application_command_seq_no_args() {
120 | let expected = Application {
121 | command: ApplicationCommand {
122 | program: "mycommand".to_owned(),
123 | args: vec![],
124 | },
125 | working_directory: None,
126 | exec: None,
127 | };
128 |
129 | equivalent! {
130 | r#"command = ["mycommand"]"#,
131 | expected;
132 | Application
133 | }
134 | }
135 |
136 | #[test]
137 | #[should_panic(expected = "command can not be empty")]
138 | fn application_command_empty_seq() {
139 | toml::from_str::(r#"command = []"#).unwrap();
140 | }
141 |
142 | #[test]
143 | fn application_command_map() {
144 | let expected = Application {
145 | command: ApplicationCommand {
146 | program: "mycommand".to_owned(),
147 | args: vec!["--with".to_owned(), "multiple args".to_owned()],
148 | },
149 | working_directory: None,
150 | exec: None,
151 | };
152 |
153 | equivalent! {
154 | r#"command = { program = "mycommand", args = ["--with", "multiple args"] }"#,
155 | expected;
156 | Application
157 | }
158 | }
159 |
160 | #[test]
161 | fn application_command_map_no_args() {
162 | let expected = Application {
163 | command: ApplicationCommand {
164 | program: "mycommand".to_owned(),
165 | args: vec![],
166 | },
167 | working_directory: None,
168 | exec: None,
169 | };
170 |
171 | equivalent! {
172 | r#"command = { program = "mycommand" }"#,
173 | expected;
174 | Application
175 | }
176 | }
177 |
178 | #[test]
179 | fn duration_secs() {
180 | equivalent! {
181 | r#"commands = []
182 | timeout = 10"#,
183 | Exec {
184 | commands: vec![],
185 | exec_type: ExecType::Text,
186 | timeout: Duration::from_secs(10),
187 | };
188 | Exec
189 | }
190 | }
191 |
192 | #[test]
193 | fn duration_map() {
194 | equivalent! {
195 | r#"commands = []
196 | timeout = { secs = 10, nanos = 42 }"#,
197 | Exec {
198 | commands: vec![],
199 | exec_type: ExecType::Text,
200 | timeout: Duration::new(10, 42),
201 | };
202 | Exec
203 | }
204 | }
205 |
206 | #[test]
207 | #[should_panic(expected = "invalid type: string")]
208 | fn duration_str() {
209 | toml::from_str::(
210 | r#"
211 | commands = []
212 | timeout = "10"
213 | "#,
214 | )
215 | .unwrap();
216 | }
217 |
218 | #[test]
219 | fn exec_commands_only() {
220 | let expected = Exec {
221 | commands: vec!["command one".to_owned(), "command two".to_owned()],
222 | exec_type: ExecType::Text,
223 | timeout: Duration::from_secs(5),
224 | };
225 |
226 | equivalent! {
227 | r#"commands = ["command one", "command two"]"#,
228 | expected;
229 | Exec
230 | }
231 | }
232 |
233 | #[test]
234 | fn exec_commands_and_type() {
235 | let expected = Exec {
236 | commands: vec!["command one".to_owned(), "command two".to_owned()],
237 | exec_type: ExecType::TextNoReturn,
238 | timeout: Duration::from_secs(5),
239 | };
240 |
241 | equivalent! {
242 | r#"
243 | commands = ["command one", "command two"]
244 | exec_type = "text_no_return"
245 | "#,
246 | expected;
247 | Exec
248 | }
249 | }
250 |
251 | #[test]
252 | fn exec_commands_type_and_timeout() {
253 | let expected = Exec {
254 | commands: vec!["command one".to_owned(), "command two".to_owned()],
255 | exec_type: ExecType::TextNoReturn,
256 | timeout: Duration::from_secs(10),
257 | };
258 |
259 | equivalent! {
260 | r#"
261 | commands = ["command one", "command two"]
262 | exec_type = "text_no_return"
263 | timeout = 10
264 | "#,
265 | expected;
266 | Exec
267 | }
268 | }
269 |
270 | #[test]
271 | fn exec_str() {
272 | let expected = Application {
273 | command: ApplicationCommand {
274 | program: "-".to_owned(),
275 | args: vec![],
276 | },
277 | working_directory: None,
278 | exec: Some(Exec {
279 | commands: vec!["command one".to_owned()],
280 | exec_type: ExecType::Text,
281 | timeout: Duration::from_secs(5),
282 | }),
283 | };
284 |
285 | equivalent! {
286 | r#"
287 | command = "-"
288 | exec = "command one"
289 | "#,
290 | expected;
291 | Application
292 | }
293 | }
294 |
295 | #[test]
296 | fn exec_seq() {
297 | let expected = Application {
298 | command: ApplicationCommand {
299 | program: "-".to_owned(),
300 | args: vec![],
301 | },
302 | working_directory: None,
303 | exec: Some(Exec {
304 | commands: vec!["command one".to_owned(), "command two".to_owned()],
305 | exec_type: ExecType::Text,
306 | timeout: Duration::from_secs(5),
307 | }),
308 | };
309 |
310 | equivalent! {
311 | r#"
312 | command = "-"
313 | exec = ["command one", "command two"]
314 | "#,
315 | expected;
316 | Application
317 | }
318 | }
319 |
--------------------------------------------------------------------------------