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

tamada / btmeister / 13048553383

30 Jan 2025 08:40AM UTC coverage: 90.179% (-0.3%) from 90.508%
13048553383

push

github

web-flow
Merge pull request #65 from tamada/release/v0.7.3

Release/v0.7.3

194 of 227 new or added lines in 6 files covered. (85.46%)

2 existing lines in 1 file now uncovered.

1414 of 1568 relevant lines covered (90.18%)

19.87 hits per line

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

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

5
use btmeister::{IgnoreType, LogLevel, 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(
20
        short,
21
        long,
22
        value_name = "LEVEL",
23
        default_value = "warn",
24
        help = "Specify the log level.",
25
        ignore_case = true
26
    )]
NEW
27
    pub(crate) level: LogLevel,
×
28

29
    #[cfg(debug_assertions)]
30
    #[clap(flatten)]
31
    pub(crate) compopts: CompletionOpts,
32
}
33

34
#[cfg(debug_assertions)]
35
#[derive(Parser, Debug)]
36
pub(crate) struct CompletionOpts {
37
    #[arg(
38
        long = "generate-completion-files",
39
        help = "Generate completion files",
40
        hide = true
41
    )]
42
    pub(crate) completion: bool,
×
43

44
    #[arg(
45
        long = "completion-out-dir",
46
        value_name = "DIR",
47
        default_value = "assets/completions",
48
        help = "Output directory of completion files",
49
        hide = true
50
    )]
51
    pub(crate) dest: PathBuf,
×
52
}
53

54
#[derive(Parser, Debug, Clone)]
55
pub(crate) struct InputOpts {
56
    #[arg(
57
        short = 'i',
58
        long = "ignore-type",
59
        default_value = "default",
60
        ignore_case = true,
61
        value_enum,
62
        value_name = "IGNORE_TYPE",
63
        help = "Specify the ignore type."
64
    )]
65
    pub(crate) ignore_types: Vec<IgnoreType>,
9✔
66

67
    #[arg(
68
        short,
69
        long,
70
        value_name = "EXCLUDEs",
71
        help = "Specify the filters of excluding files or directories."
72
    )]
NEW
73
    pub(crate) excludes: Vec<String>,
×
74

75
    #[arg(
76
        value_name = "PROJECTs",
77
        required = false,
78
        help = "The target project paths. If \"-\" was given, reads from stdin.
79
Also, the first character was \"@\", read from the file eliminating \"@\".
80
This parameters accept directories and archive files.
81
Supported archive files: tar, tar.bz2, tar.gz, tar.xz, tar.zstd, and zip."
82
    )]
83
    pub dirs: Vec<String>,
8✔
84
}
85

86
#[derive(Parser, Debug, Clone)]
87
pub(crate) struct OutputOpts {
88
    #[arg(
89
        short = 'L',
90
        long = "list-defs",
91
        help = "Print the build tools' definition list"
92
    )]
93
    pub(crate) list_defs: bool,
×
94

95
    #[arg(
96
        short,
97
        long,
98
        default_value_t = Format::Default,
1✔
99
        value_name = "FORMAT",
100
        value_enum,
101
        ignore_case = true,
102
        help = "Specify the output format"
103
    )]
104
    pub(crate) format: Format,
×
105
}
106

107
#[derive(Parser, Debug, Clone)]
108
pub(crate) struct DefOpts {
109
    #[arg(
110
        short = 'D',
111
        long,
112
        value_name = "DEFS_JSON",
113
        help = "Specify the definition of the build tools."
114
    )]
115
    pub(crate) definition: Option<PathBuf>,
116

117
    #[arg(
118
        long,
119
        value_name = "DEFS_JSON",
120
        help = "Specify the additional definitions of the build tools."
121
    )]
122
    pub(crate) append_defs: Option<PathBuf>,
123
}
124

125
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
126
pub enum Format {
127
    Csv,
128
    Default,
129
    Json,
130
    Markdown,
131
    Xml,
132
    Yaml,
133
}
134

135
fn read_from_reader(r: Box<dyn BufRead>) -> Result<Vec<String>> {
2✔
136
    let result = r
2✔
137
        .lines()
2✔
138
        .map_while(|r| r.ok())
13✔
139
        .map(|l| l.trim().to_string())
13✔
140
        .filter(|l| !l.starts_with("#") && !l.is_empty())
13✔
141
        .collect::<Vec<String>>();
2✔
142
    Ok(result)
2✔
143
}
2✔
144

145
fn read_from_stdin() -> Result<Vec<String>> {
×
146
    read_from_reader(Box::new(io::stdin().lock()))
×
147
}
×
148

149
fn read_from_file(filename: &str) -> Result<Vec<String>> {
3✔
150
    match std::fs::File::open(filename) {
3✔
151
        Err(e) => Err(MeisterError::IO(e)),
1✔
152
        Ok(file) => read_from_reader(Box::new(std::io::BufReader::new(file))),
2✔
153
    }
154
}
3✔
155

156
fn convert_and_push_item(item: &str, result: &mut Vec<PathBuf>, errs: &mut Vec<MeisterError>) {
11✔
157
    let path = PathBuf::from(item);
11✔
158
    if !path.exists() {
11✔
159
        errs.push(MeisterError::ProjectNotFound(item.to_string()));
3✔
160
    } else if path.is_file() {
8✔
161
        if btmeister::is_supported_archive_format(&path) {
1✔
162
            result.push(path);
×
163
        } else {
1✔
164
            errs.push(MeisterError::ProjectNotFound(item.to_string()));
1✔
165
        }
1✔
166
    } else if path.is_dir() {
7✔
167
        result.push(path);
7✔
168
    } else {
7✔
169
        errs.push(MeisterError::ProjectNotFound(item.to_string()));
×
170
    }
×
171
}
11✔
172

173
fn push_items_or_errs(
3✔
174
    r: Result<Vec<String>>,
3✔
175
    results: &mut Vec<PathBuf>,
3✔
176
    errs: &mut Vec<MeisterError>,
3✔
177
) {
3✔
178
    match r {
3✔
179
        Err(e) => errs.push(e),
1✔
180
        Ok(items) => {
2✔
181
            for item in items {
8✔
182
                convert_and_push_item(&item, results, errs)
6✔
183
            }
184
        }
185
    }
186
}
3✔
187

188
impl InputOpts {
189
    pub(crate) fn projects(&self) -> Result<Vec<PathBuf>> {
8✔
190
        let mut errs = vec![];
8✔
191
        let mut result = vec![];
8✔
192
        for item in self.dirs.iter() {
8✔
193
            if item == "-" {
8✔
194
                push_items_or_errs(read_from_stdin(), &mut result, &mut errs);
×
195
            } else if let Some(stripped) = item.strip_prefix('@') {
8✔
196
                push_items_or_errs(read_from_file(stripped), &mut result, &mut errs);
3✔
197
            } else {
5✔
198
                convert_and_push_item(item.as_str(), &mut result, &mut errs);
5✔
199
            }
5✔
200
        }
201
        if !errs.is_empty() {
8✔
202
            Err(MeisterError::Array(errs))
4✔
203
        } else if result.is_empty() {
4✔
204
            Err(MeisterError::NoProjectSpecified())
1✔
205
        } else {
206
            Ok(result)
3✔
207
        }
208
    }
8✔
209
}
210

211
#[cfg(test)]
212
mod tests {
213
    use super::*;
214

215
    #[test]
216
    fn test_projects1() {
1✔
217
        let opts = Options::parse_from(&["meister", "testdata/fibonacci", "testdata/hello"]);
1✔
218
        let projects = opts.inputs.projects();
1✔
219
        assert!(projects.is_ok());
1✔
220
        if let Ok(p) = projects {
1✔
221
            assert_eq!(2, p.len());
1✔
222
            assert_eq!(PathBuf::from("testdata/fibonacci"), p[0]);
1✔
223
            assert_eq!(PathBuf::from("testdata/hello"), p[1]);
1✔
224
        }
×
225
    }
1✔
226

227
    #[test]
228
    fn test_projects2() {
1✔
229
        let opts = Options::parse_from(&["meister", "@testdata/project_list.txt"]);
1✔
230
        let projects = opts.inputs.projects();
1✔
231
        assert!(projects.is_ok());
1✔
232
        if let Ok(p) = projects {
1✔
233
            assert_eq!(2, p.len());
1✔
234
            assert_eq!(PathBuf::from("testdata/hello"), p[0]);
1✔
235
            assert_eq!(PathBuf::from("testdata/fibonacci"), p[1]);
1✔
236
        }
×
237
    }
1✔
238

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

252
    #[test]
253
    fn test_invalid_project_list() {
1✔
254
        let opts = Options::parse_from(&["meister", "@testdata/invalid_project_list.txt"]);
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!(2, e.len());
1✔
259
            if let MeisterError::ProjectNotFound(p) = &e[0] {
1✔
260
                assert_eq!("not_exist_project", p);
1✔
261
            }
×
262
            if let MeisterError::ProjectNotFound(p) = &e[1] {
1✔
263
                assert_eq!("testdata/project_list.txt", p);
1✔
264
            }
×
265
        }
×
266
    }
1✔
267

268
    #[test]
269
    fn test_unknownfile() {
1✔
270
        let opts = Options::parse_from(&["meister", "@unknownfile"]);
1✔
271
        let projects = opts.inputs.projects();
1✔
272
        assert!(projects.is_err());
1✔
273
        if let Err(MeisterError::Array(e)) = projects {
1✔
274
            assert_eq!(1, e.len());
1✔
275
            if let MeisterError::IO(p) = &e[0] {
1✔
276
                assert_eq!(std::io::ErrorKind::NotFound, p.kind());
1✔
277
            }
×
278
        }
×
279
    }
1✔
280

281
    #[test]
282
    fn test_no_projects() {
1✔
283
        let opts = InputOpts {
1✔
284
            ignore_types: vec![],
1✔
285
            excludes: vec![],
1✔
286
            dirs: vec![],
1✔
287
        };
1✔
288
        let projects = opts.projects();
1✔
289
        assert!(projects.is_err());
1✔
290
        match projects {
1✔
291
            Err(MeisterError::NoProjectSpecified()) => assert!(true),
1✔
292
            _ => assert!(false),
×
293
        }
294
    }
1✔
295
}
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