• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tamada / btmeister / 12789865655

15 Jan 2025 01:58PM UTC coverage: 90.508% (-1.2%) from 91.68%
12789865655

push

github

web-flow
Merge pull request #63 from tamada/release/v0.7.1

Release/v0.7.1

1335 of 1475 relevant lines covered (90.51%)

24.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.82
/src/cli.rs
1
use clap::{Parser, ValueEnum};
2
use std::io::{self, BufRead};
3
use std::path::PathBuf;
4

5
use btmeister::{IgnoreType, MeisterError, Result};
6

7
#[derive(Parser, Debug)]
8
#[clap(author, version, about, arg_required_else_help = true)]
9
pub(crate) struct Options {
10
    #[clap(flatten)]
11
    pub(crate) defopts: DefOpts,
12

13
    #[clap(flatten)]
14
    pub(crate) inputs: InputOpts,
15

16
    #[clap(flatten)]
17
    pub(crate) outputs: OutputOpts,
18

19
    #[arg(short, long, help = "Show verbose output.")]
20
    pub(crate) verbose: bool,
×
21

22
    #[clap(flatten)]
23
    pub(crate) compopts: CompletionOpts,
24
}
25

26
#[derive(Parser, Debug)]
27
pub(crate) struct CompletionOpts {
28
    #[arg(
29
        long = "generate-completion-files",
30
        help = "Generate completion files",
31
        hide = true
32
    )]
33
    pub(crate) completion: bool,
×
34

35
    #[arg(
36
        long = "completion-out-dir",
37
        value_name = "DIR",
38
        default_value = "assets/completions",
39
        help = "Output directory of completion files",
40
        hide = true
41
    )]
42
    pub(crate) dest: PathBuf,
×
43
}
44

45
#[derive(Parser, Debug, Clone)]
46
pub(crate) struct InputOpts {
47
    #[arg(
48
        short = 'i',
49
        long = "ignore-type",
50
        default_value = "default",
51
        ignore_case = true,
52
        value_enum,
53
        value_name = "IGNORE_TYPE",
54
        help = "specify the ignore type."
55
    )]
56
    pub(crate) ignore_types: Vec<IgnoreType>,
9✔
57

58
    #[arg(
59
        value_name = "PROJECTs",
60
        required = false,
61
        help = "The target project paths. If \"-\" was given, reads from stdin.
62
Also, the first character was \"@\", read from the file eliminating \"@\".
63
This parameters accept directories and archive files.
64
Supported archive files: tar, tar.bz2, tar.gz, tar.xz, tar.zstd, and zip."
65
    )]
66
    pub dirs: Vec<String>,
8✔
67
}
68

69
#[derive(Parser, Debug, Clone)]
70
pub(crate) struct OutputOpts {
71
    #[arg(
72
        short = 'L',
73
        long = "list-defs",
74
        help = "Print the build tools' definition list"
75
    )]
76
    pub(crate) list_defs: bool,
×
77

78
    #[arg(
79
        short,
80
        long,
81
        default_value_t = Format::Default,
1✔
82
        value_name = "FORMAT",
83
        value_enum,
84
        ignore_case = true,
85
        help = "Specify the output format"
86
    )]
87
    pub(crate) format: Format,
×
88
}
89

90
#[derive(Parser, Debug, Clone)]
91
pub(crate) struct DefOpts {
92
    #[arg(
93
        short = 'D',
94
        long,
95
        value_name = "DEFS_JSON",
96
        help = "Specify the definition of the build tools."
97
    )]
98
    pub(crate) definition: Option<PathBuf>,
99

100
    #[arg(
101
        long,
102
        value_name = "DEFS_JSON",
103
        help = "Specify the additional definitions of the build tools."
104
    )]
105
    pub(crate) append_defs: Option<PathBuf>,
106
}
107

108
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
109
pub enum Format {
110
    Csv,
111
    Default,
112
    Json,
113
    Markdown,
114
    Xml,
115
    Yaml,
116
}
117

118
fn read_from_reader(r: Box<dyn BufRead>) -> Result<Vec<String>> {
2✔
119
    let mut result = vec![];
2✔
120
    for line in r.lines().flatten() {
13✔
121
        if line.starts_with("#") || line.trim().is_empty() {
13✔
122
            continue;
7✔
123
        }
6✔
124
        result.push(line);
6✔
125
    }
126
    Ok(result)
2✔
127
}
2✔
128

129
fn read_from_stdin() -> Result<Vec<String>> {
×
130
    read_from_reader(Box::new(io::stdin().lock()))
×
131
}
×
132

133
fn read_from_file(filename: &str) -> Result<Vec<String>> {
3✔
134
    match std::fs::File::open(filename) {
3✔
135
        Err(e) => Err(MeisterError::IO(e)),
1✔
136
        Ok(file) => read_from_reader(Box::new(std::io::BufReader::new(file))),
2✔
137
    }
138
}
3✔
139

140
fn convert_and_push_item(item: &str, result: &mut Vec<PathBuf>, errs: &mut Vec<MeisterError>) {
11✔
141
    let path = PathBuf::from(item);
11✔
142
    if !path.exists() {
11✔
143
        errs.push(MeisterError::ProjectNotFound(item.to_string()));
3✔
144
    } else if path.is_file() {
8✔
145
        if btmeister::is_supported_archive_format(&path) {
1✔
146
            result.push(path);
×
147
        } else {
1✔
148
            errs.push(MeisterError::ProjectNotFound(item.to_string()));
1✔
149
        }
1✔
150
    } else if path.is_dir() {
7✔
151
        result.push(path);
7✔
152
    } else {
7✔
153
        errs.push(MeisterError::ProjectNotFound(item.to_string()));
×
154
    }
×
155
}
11✔
156

157
fn push_items_or_errs(
3✔
158
    r: Result<Vec<String>>,
3✔
159
    results: &mut Vec<PathBuf>,
3✔
160
    errs: &mut Vec<MeisterError>,
3✔
161
) {
3✔
162
    match r {
3✔
163
        Err(e) => errs.push(e),
1✔
164
        Ok(items) => {
2✔
165
            for item in items {
8✔
166
                convert_and_push_item(&item, results, errs)
6✔
167
            }
168
        }
169
    }
170
}
3✔
171

172
impl InputOpts {
173
    pub(crate) fn projects(&self) -> Result<Vec<PathBuf>> {
8✔
174
        let mut errs = vec![];
8✔
175
        let mut result = vec![];
8✔
176
        for item in self.dirs.iter() {
8✔
177
            if item == "-" {
8✔
178
                push_items_or_errs(read_from_stdin(), &mut result, &mut errs);
×
179
            } else if let Some(stripped) = item.strip_prefix('@') {
8✔
180
                push_items_or_errs(read_from_file(stripped), &mut result, &mut errs);
3✔
181
            } else {
5✔
182
                convert_and_push_item(item.as_str(), &mut result, &mut errs);
5✔
183
            }
5✔
184
        }
185
        if !errs.is_empty() {
8✔
186
            Err(MeisterError::Array(errs))
4✔
187
        } else if result.is_empty() {
4✔
188
            Err(MeisterError::NoProjectSpecified())
1✔
189
        } else {
190
            Ok(result)
3✔
191
        }
192
    }
8✔
193
}
194

195
#[cfg(test)]
196
mod tests {
197
    use super::*;
198

199
    #[test]
200
    fn test_projects1() {
1✔
201
        let opts = Options::parse_from(&["meister", "testdata/fibonacci", "testdata/hello"]);
1✔
202
        let projects = opts.inputs.projects();
1✔
203
        assert!(projects.is_ok());
1✔
204
        if let Ok(p) = projects {
1✔
205
            assert_eq!(2, p.len());
1✔
206
            assert_eq!(PathBuf::from("testdata/fibonacci"), p[0]);
1✔
207
            assert_eq!(PathBuf::from("testdata/hello"), p[1]);
1✔
208
        }
×
209
    }
1✔
210

211
    #[test]
212
    fn test_projects2() {
1✔
213
        let opts = Options::parse_from(&["meister", "@testdata/project_list.txt"]);
1✔
214
        let projects = opts.inputs.projects();
1✔
215
        assert!(projects.is_ok());
1✔
216
        if let Ok(p) = projects {
1✔
217
            assert_eq!(2, p.len());
1✔
218
            assert_eq!(PathBuf::from("testdata/hello"), p[0]);
1✔
219
            assert_eq!(PathBuf::from("testdata/fibonacci"), p[1]);
1✔
220
        }
×
221
    }
1✔
222

223
    #[test]
224
    fn test_not_exist_project() {
1✔
225
        let opts = Options::parse_from(&["meister", "not_exist_project"]);
1✔
226
        let projects = opts.inputs.projects();
1✔
227
        assert!(projects.is_err());
1✔
228
        if let Err(MeisterError::Array(e)) = projects {
1✔
229
            assert_eq!(1, e.len());
1✔
230
            if let MeisterError::ProjectNotFound(p) = &e[0] {
1✔
231
                assert_eq!("not_exist_project", p);
1✔
232
            }
×
233
        }
×
234
    }
1✔
235

236
    #[test]
237
    fn test_invalid_project_list() {
1✔
238
        let opts = Options::parse_from(&["meister", "@testdata/invalid_project_list.txt"]);
1✔
239
        let projects = opts.inputs.projects();
1✔
240
        assert!(projects.is_err());
1✔
241
        if let Err(MeisterError::Array(e)) = projects {
1✔
242
            assert_eq!(2, e.len());
1✔
243
            if let MeisterError::ProjectNotFound(p) = &e[0] {
1✔
244
                assert_eq!("not_exist_project", p);
1✔
245
            }
×
246
            if let MeisterError::ProjectNotFound(p) = &e[1] {
1✔
247
                assert_eq!("testdata/project_list.txt", p);
1✔
248
            }
×
249
        }
×
250
    }
1✔
251

252
    #[test]
253
    fn test_unknownfile() {
1✔
254
        let opts = Options::parse_from(&["meister", "@unknownfile"]);
1✔
255
        let projects = opts.inputs.projects();
1✔
256
        assert!(projects.is_err());
1✔
257
        if let Err(MeisterError::Array(e)) = projects {
1✔
258
            assert_eq!(1, e.len());
1✔
259
            if let MeisterError::IO(p) = &e[0] {
1✔
260
                assert_eq!(std::io::ErrorKind::NotFound, p.kind());
1✔
261
            }
×
262
        }
×
263
    }
1✔
264

265
    #[test]
266
    fn test_no_projects() {
1✔
267
        let opts = InputOpts {
1✔
268
            ignore_types: vec![],
1✔
269
            dirs: vec![],
1✔
270
        };
1✔
271
        let projects = opts.projects();
1✔
272
        assert!(projects.is_err());
1✔
273
        match projects {
1✔
274
            Err(MeisterError::NoProjectSpecified()) => assert!(true),
1✔
275
            _ => assert!(false),
×
276
        }
277
    }
1✔
278
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc