Coverage Report

Created: 2024-02-20 21:15

/builds/xfbs/cindy/src/cli.rs
Line
Count
Source (jump to first uncovered line)
1
use crate::tag::{Tag, TagFilter, TagPredicate};
2
use clap::Parser;
3
use std::{net::SocketAddr, path::PathBuf};
4
5
/// Global options.
6
512
#[derive(
P52
arse
r26
,
Clone256
,
Debug256
, Default)]
7
pub struct GlobalOptions {
8
    /// Configuration file.
9
    ///
10
    /// By default, Cindy will automatically discover this by walking up the current working
11
    /// directory until it finds a cindy.toml file.
12
    #[clap(long, env, global = true)]
13
    pub config: Option<PathBuf>,
14
15
    /// Turn on verbose output.
16
    #[clap(long, short, global = true)]
17
0
    pub verbose: bool,
18
19
    /// Turn on verbose output.
20
    #[clap(long, short = 'j', global = true)]
21
    pub threads: Option<u64>,
22
}
23
24
/// Cindy command-line options.
25
256
#[derive(
P26
arse
r26
, Clone, Debug)]
26
pub struct Options {
27
    /// Global options, shared by all subcommands.
28
    #[clap(flatten)]
29
    pub global: GlobalOptions,
30
31
    /// Subcommand to run.
32
    #[clap(subcommand)]
33
    pub command: Command,
34
}
35
36
/// Initialize new Cindy project.
37
109
#[derive(
P28
arse
r26
, Clone,
Debug84
)]
38
pub struct InitCommand {
39
    #[clap(default_value = ".")]
40
0
    pub path: PathBuf,
41
}
42
43
/// Serve Cindy web user interface.
44
26
#[derive(Parser, 
Clone0
,
Debug0
)]
45
pub struct ServeCommand {
46
    #[clap(short, long, default_value = "127.0.0.1:8000")]
47
0
    pub listen: SocketAddr,
48
}
49
50
85
#[derive(
P28
arse
r26
, Clone,
Debug83
)]
51
pub struct AddCommand {
52
    /// Add files recursively.
53
    #[clap(long, short)]
54
0
    pub recursive: bool,
55
56
    #[clap(default_value = ".")]
57
2
    pub paths: Vec<PathBuf>,
58
}
59
60
85
#[derive(
P28
arse
r26
,
Clone79
, Debug)]
61
pub struct QueryCommand {
62
    /// Show tags of each media.
63
    #[clap(long)]
64
0
    pub tags: bool,
65
66
    /// Show paths instead of the hashes.
67
    #[clap(long)]
68
0
    pub paths: bool,
69
70
2
    pub filters: Vec<TagPredicate<'static>>,
71
}
72
73
79
#[derive(
P34
arse
r26
,
Clone72
, Debug)]
74
pub struct ListCommand {
75
    #[clap(default_value = ".")]
76
0
    pub path: PathBuf,
77
78
    #[clap(long, short)]
79
0
    pub recursive: bool,
80
}
81
82
96
#[derive(
P28
arse
r26
, Clone,
Debug88
)]
83
pub struct EditCommand {
84
    /// Tag to add.
85
    #[clap(long, short)]
86
1
    pub add: Vec<Tag>,
87
88
    /// Tag to remove.
89
    #[clap(long, short = 'd')]
90
1
    pub remove: Vec<Tag>,
91
92
    /// Apply to files recursively.
93
    #[clap(long, short)]
94
0
    pub recursive: bool,
95
96
    /// List of files to apply it to.
97
2
    pub files: Vec<PathBuf>,
98
}
99
100
93
#[derive(
P30
arse
r26
,
Clone72
, Debug)]
101
pub struct RemoveCommand {
102
    #[clap(long, short)]
103
0
    pub recursive: bool,
104
105
    #[clap(default_value = ".")]
106
4
    pub paths: Vec<PathBuf>,
107
}
108
109
1
#[derive(Parser, Clone, 
Debug0
)]
110
pub struct TagsCreateCommand {
111
1
    pub tags: Vec<Tag>,
112
}
113
114
28
#[derive(Parse
r26
,
Clone0
,
Debug0
)]
115
pub struct TagsDeleteCommand {
116
2
    pub tags: Vec<TagFilter<'static>>,
117
118
    /// Force deleting a tag if it is still in use.
119
    #[clap(short, long)]
120
0
    pub force: bool,
121
}
122
123
27
#[derive(Parse
r26
,
Clone0
,
Debug0
)]
124
pub struct TagsRenameCommand {
125
0
    pub old: TagFilter<'static>,
126
0
    pub new: TagFilter<'static>,
127
}
128
129
2
#[derive(Parser, 
Clone1
,
Debug0
)]
130
pub struct TagsListCommand {
131
1
    pub tags: Vec<TagFilter<'static>>,
132
}
133
134
16
#[derive(
P0
arser,
C0
lon
e0
,
D0
ebu
g0
)]
135
pub enum TagsCommand {
136
    /// Create a new tag.
137
    Create(TagsCreateCommand),
138
    /// Delete a tag.
139
    Delete(TagsDeleteCommand),
140
    /// Rename a tag.
141
    Rename(TagsRenameCommand),
142
    /// List tags.
143
    List(TagsListCommand),
144
}
145
146
512
#[derive(
P0
arse
r120
, Clone, Debug)]
147
pub enum Command {
148
    /// Initialize new Cindy project.
149
    Init(InitCommand),
150
    /// Add files to the Cindy index.
151
    Add(AddCommand),
152
    /// Remove files from the Cindy index.
153
    #[clap(alias = "rm")]
154
    Remove(RemoveCommand),
155
    /// Query files in the Cindy project.
156
    Query(QueryCommand),
157
    /// List files
158
    #[clap(alias = "ls")]
159
    List(ListCommand),
160
    /// Manage tags for files.
161
    Edit(EditCommand),
162
    /// Manage tags
163
    #[clap(subcommand)]
164
    Tags(TagsCommand),
165
    /// Serve Cindy UI.
166
    #[cfg(feature = "server")]
167
    #[clap(alias = "server")]
168
    Serve(ServeCommand),
169
}
170
171
#[cfg(test)]
172
mod tests {
173
    use super::*;
174
    use proptest::prelude::*;
175
176
8
    fn arb_tag_filter() -> impl Strategy<Value = TagFilter<'static>> {
177
8
        prop_oneof![
178
8
            Just(TagFilter::new::<&str>(None, None)),
179
194
            "[a-z]{4}".prop_map(|string| TagFilter::new::<String>(Some(string), None)),
180
174
            "[a-z]{4}".prop_map(|string| TagFilter::new::<String>(None, Some(string))),
181
8
            ("[a-z]{4}", "[a-z]{4}")
182
168
                .prop_map(|(name, value)| TagFilter::new::<String>(Some(name), Some(value))),
183
8
        ]
184
8
        .boxed()
185
8
    }
186
187
8
    fn arb_tag() -> impl Strategy<Value = Tag> {
188
715
        ("[a-z]{4}", "[a-z]{4}").prop_map(|(name, value)| Tag::new(name, value))
189
8
    }
190
191
2
    fn arb_options() -> impl Strategy<Value = Options> {
192
512
        arb_command().prop_map(|command| Options {
193
512
            global: Default::default(),
194
512
            command,
195
512
        })
196
2
    }
197
198
4
    fn arb_tag_predicate() -> impl Strategy<Value = TagPredicate<'static>> {
199
4
        prop_oneof![
200
4
            arb_tag_filter().prop_map(TagPredicate::Exists),
201
4
            arb_tag_filter().prop_map(TagPredicate::Missing),
202
4
        ]
203
4
    }
204
205
20
    fn arb_path_buf() -> impl Strategy<Value = PathBuf> {
206
20
        prop_oneof![
207
20
            Just(PathBuf::from(".")),
208
513
            "[a-z]{4}".prop_map(|seg| PathBuf::from(seg)),
209
512
            ("[a-z]{4}", "[a-z]{4}").prop_map(|(seg1, seg2)| PathBuf::from(seg1).join(seg2)),
210
20
            ("[a-z]{4}", "[a-z]{4}", "[a-z]{4}")
211
498
                .prop_map(|(seg1, seg2, seg3)| PathBuf::from(seg1).join(seg2).join(seg3)),
212
20
        ]
213
20
    }
214
215
8
    fn arb_path_buf_list_or_pwd() -> impl Strategy<Value = Vec<PathBuf>> {
216
8
        prop_oneof![
217
8
            Just(vec![PathBuf::from(".")]),
218
8
            prop::collection::vec(arb_path_buf(), 1..10),
219
8
        ]
220
8
    }
221
222
193
    prop_compose! {
223
193
        fn arb_init_command()(path in arb_path_buf()) -> InitCommand {
224
193
            InitCommand {
225
193
                path,
226
193
            }
227
193
        }
228
193
    }
229
230
151
    prop_compose! {
231
151
        fn arb_list_command()(recursive in prop::bool::ANY, path in arb_path_buf()) -> ListCommand {
232
151
            ListCommand {
233
151
                path,
234
151
                recursive,
235
151
            }
236
151
        }
237
151
    }
238
239
163
    prop_compose! {
240
163
        fn arb_query_command()(filters in prop::collection::vec(arb_tag_predicate(), 0..10)) -> QueryCommand {
241
163
            QueryCommand {
242
163
                filters,
243
163
                tags: false,
244
163
                paths: false,
245
163
            }
246
163
        }
247
163
    }
248
249
184
    prop_compose! {
250
184
        fn arb_edit_command()(
251
184
            files in prop::collection::vec(arb_path_buf(), 1..10),
252
184
            add in prop::collection::vec(arb_tag(), 0..5),
253
184
            remove in prop::collection::vec(arb_tag(), 0..5),
254
184
            recursive in prop::bool::ANY
255
184
        ) -> EditCommand {
256
184
            EditCommand {
257
184
                recursive,
258
184
                add,
259
184
                remove,
260
184
                files,
261
184
            }
262
184
        }
263
184
    }
264
265
168
    prop_compose! {
266
168
        fn arb_add_command()(recursive in prop::bool::ANY, paths in arb_path_buf_list_or_pwd()) -> AddCommand {
267
168
            AddCommand {
268
168
                recursive,
269
168
                paths,
270
168
            }
271
168
        }
272
168
    }
273
274
165
    prop_compose! {
275
165
        fn arb_remove_command()(recursive in prop::bool::ANY, paths in arb_path_buf_list_or_pwd()) -> RemoveCommand {
276
165
            RemoveCommand {
277
165
                recursive,
278
165
                paths,
279
165
            }
280
165
        }
281
165
    }
282
283
4
    fn arb_command() -> impl Strategy<Value = Command> {
284
4
        prop_oneof![
285
4
            arb_init_command().prop_map(Command::Init),
286
4
            arb_add_command().prop_map(Command::Add),
287
4
            arb_remove_command().prop_map(Command::Remove),
288
4
            arb_query_command().prop_map(Command::Query),
289
4
            arb_list_command().prop_map(Command::List),
290
4
            arb_edit_command().prop_map(Command::Edit),
291
4
        ]
292
4
    }
293
294
2.05k
    proptest! {
295
2.05k
        #[test]
296
2.05k
        fn command_clone(command in arb_command()) {
297
2.05k
            let _command_clone = command.clone();
298
2.05k
        }
299
2.05k
300
2.05k
        #[test]
301
2.05k
        fn command_debug(command in arb_command()) {
302
2.05k
            let _command_debug = format!("{command:?}");
303
2.05k
        }
304
2.05k
305
2.05k
        #[test]
306
2.05k
        fn options_clone(options in arb_options()) {
307
2.05k
            let _options_clone = options.clone();
308
2.05k
        }
309
2.05k
310
2.05k
        #[test]
311
2.05k
        fn options_debug(options in arb_options()) {
312
2.05k
            let _options_debug = format!("{options:?}");
313
2.05k
        }
314
2.05k
    }
315
316
1
    #[test]
317
1
    fn cli_examples() {
318
1
        // initialize new project
319
1
        Options::try_parse_from(&["cindy", "init"]).unwrap();
320
1
        Options::try_parse_from(&["cindy", "init", "folder"]).unwrap();
321
1
322
1
        // add files (recursively)
323
1
        Options::try_parse_from(&["cindy", "add", "file1", "file2"]).unwrap();
324
1
        Options::try_parse_from(&["cindy", "add", "-r", "folder"]).unwrap();
325
1
326
1
        // remove files (recursively)
327
1
        Options::try_parse_from(&["cindy", "remove", "file1", "file2"]).unwrap();
328
1
        Options::try_parse_from(&["cindy", "remove", "-r", "folder"]).unwrap();
329
1
        Options::try_parse_from(&["cindy", "rm", "file1", "file2"]).unwrap();
330
1
        Options::try_parse_from(&["cindy", "rm", "-r", "folder"]).unwrap();
331
1
332
1
        // query files
333
1
        Options::try_parse_from(&["cindy", "query", "name:value", "name:other"]).unwrap();
334
1
        Options::try_parse_from(&["cindy", "query", "name:value", "!name:other"]).unwrap();
335
1
336
1
        // list files
337
1
        Options::try_parse_from(&["cindy", "list"]).unwrap();
338
1
        Options::try_parse_from(&["cindy", "list", "folder"]).unwrap();
339
1
        Options::try_parse_from(&["cindy", "list", "-r"]).unwrap();
340
1
        Options::try_parse_from(&["cindy", "list", "-r", "folder"]).unwrap();
341
1
        Options::try_parse_from(&["cindy", "ls"]).unwrap();
342
1
        Options::try_parse_from(&["cindy", "ls", "folder"]).unwrap();
343
1
        Options::try_parse_from(&["cindy", "ls", "-r"]).unwrap();
344
1
        Options::try_parse_from(&["cindy", "ls", "-r", "folder"]).unwrap();
345
1
346
1
        Options::try_parse_from(&["cindy", "edit", "file", "--add", "name:value"]).unwrap();
347
1
        Options::try_parse_from(&["cindy", "edit", "file", "--remove", "name:value"]).unwrap();
348
1
349
1
        Options::try_parse_from(&["cindy", "tags", "create", "name:value"]).unwrap();
350
1
        Options::try_parse_from(&["cindy", "tags", "delete", "name:value"]).unwrap();
351
1
        Options::try_parse_from(&["cindy", "tags", "delete", "--force", "name:value"]).unwrap();
352
1
        Options::try_parse_from(&["cindy", "tags", "list"]).unwrap();
353
1
        Options::try_parse_from(&["cindy", "tags", "list", "name:*"]).unwrap();
354
1
        Options::try_parse_from(&["cindy", "tags", "rename", "name:value", "name:other"]).unwrap();
355
1
    }
356
}