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

tamada / btmeister / 18859797937

28 Oct 2025 12:13AM UTC coverage: 90.0% (-0.7%) from 90.704%
18859797937

Pull #72

github

tamada
Implement utility for generating  Filter instance and  add corresponding tests
Pull Request #72: Release/v0.8.2

146 of 174 new or added lines in 12 files covered. (83.91%)

38 existing lines in 2 files now uncovered.

1467 of 1630 relevant lines covered (90.0%)

20.47 hits per line

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

86.36
/cli/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
    )]
27
    pub(crate) level: LogLevel,
28

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

34
#[derive(Parser, Debug)]
35
pub(crate) struct FilterOpts {
36
    #[clap(short = 'I', long, value_name = "TOOL_NAME", help = "comma separated tool names to include. If start with '@', reads from file.", conflicts_with_all = [ "excludes", "include_files", "exclude_files" ])]
37
    pub(crate) includes: Option<String>,
38

39
    #[clap(short = 'E', long, value_name = "TOOL_NAME", help = "comma separated tool names to exclude. If start with '@', reads from file.", conflicts_with_all = [ "includes", "include_files", "exclude_files" ])]
40
    pub(crate) excludes: Option<String>,
41

42
    #[clap(long, value_name = "BUILD_FILE_NAME", help = "comma separated build file names to include. If start with '@', reads from file.", conflicts_with_all = [ "includes", "excludes", "exclude_files" ])]
43
    pub(crate) include_files: Option<String>,
44

45
    #[clap(long, value_name = "BUILD_FILE_NAME", help = "comma separated build file names to exclude. If start with '@', reads from file.", conflicts_with_all = [ "includes", "excludes", "include_files" ])]
46
    pub(crate) exclude_files: Option<String>,
47
}
48

49
#[cfg(debug_assertions)]
50
#[derive(Parser, Debug)]
51
pub(crate) struct CompletionOpts {
52
    #[arg(
53
        long = "generate-completion-files",
54
        help = "Generate completion files",
55
        hide = true
56
    )]
57
    pub(crate) completion: bool,
58

59
    #[arg(
60
        long = "completion-out-dir",
61
        value_name = "DIR",
62
        default_value = "assets/completions",
63
        help = "Output directory of completion files",
64
        hide = true
65
    )]
66
    pub(crate) dest: PathBuf,
67
}
68

69
#[derive(Parser, Debug, Clone)]
70
pub(crate) struct InputOpts {
71
    #[arg(
72
        short = 'i',
73
        long = "ignore-type",
74
        default_value = "default",
75
        ignore_case = true,
76
        value_enum,
77
        value_name = "IGNORE_TYPE",
78
        help = "Specify the ignore type."
79
    )]
80
    pub(crate) ignore_types: Vec<IgnoreType>,
81

82
    #[arg(
83
        short = 's',
84
        long = "skip-traverse",
85
        value_name = "SKIP_DIRs",
86
        help = "Specify the skip directories."
87
    )]
88
    pub(crate) skips: Vec<String>,
89

90
    #[arg(
91
        value_name = "PROJECTs",
92
        required = false,
93
        help = "The target project paths. If \"-\" was given, reads from stdin.
94
Also, the first character was \"@\", read from the file eliminating \"@\".
95
This parameters accept directories and archive files.
96
Supported archive files: tar, tar.bz2, tar.gz, tar.xz, tar.zstd, and zip."
97
    )]
98
    pub dirs: Vec<String>,
99
}
100

101
#[derive(Parser, Debug, Clone)]
102
pub(crate) struct OutputOpts {
103
    #[arg(
104
        short = 'L',
105
        long = "list-defs",
106
        help = "Print the build tools' definition list"
107
    )]
108
    pub(crate) list_defs: bool,
109

110
    #[arg(
111
        short,
112
        long,
113
        default_value_t = Format::Default,
114
        value_name = "FORMAT",
115
        value_enum,
116
        ignore_case = true,
117
        help = "Specify the output format"
118
    )]
119
    pub(crate) format: Format,
120
}
121

122
#[derive(Parser, Debug)]
123
pub(crate) struct DefOpts {
124
    #[arg(
125
        short = 'D',
126
        long,
127
        value_name = "DEFS_JSON",
128
        help = "Specify the definition of the build tools."
129
    )]
130
    pub(crate) definition: Option<PathBuf>,
131

132
    #[arg(
133
        long,
134
        value_name = "DEFS_JSON",
135
        help = "Specify the additional definitions of the build tools."
136
    )]
137
    pub(crate) append_defs: Option<PathBuf>,
138

139
    #[clap(flatten)]
140
    pub(crate) filter: FilterOpts,
141
}
142

143
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
144
pub enum Format {
145
    Csv,
146
    Default,
147
    Json,
148
    Markdown,
149
    Xml,
150
    Yaml,
151
}
152

153
fn read_from_reader(r: Box<dyn BufRead>, parent: PathBuf) -> Result<Vec<String>> {
2✔
154
    let result = r
2✔
155
        .lines()
2✔
156
        .map_while(|r| r.ok())
14✔
157
        .map(|l| l.trim().to_string())
14✔
158
        .filter(|l| !l.starts_with("#") && !l.is_empty())
14✔
159
        .map(|name| parent.join(name).to_str().unwrap().to_string())
6✔
160
        .collect::<Vec<String>>();
2✔
161
    Ok(result)
2✔
162
}
2✔
163

164
fn read_from_stdin() -> Result<Vec<String>> {
×
165
    read_from_reader(Box::new(io::stdin().lock()), PathBuf::from("."))
×
166
}
×
167

168
fn read_from_file(filename: &str) -> Result<Vec<String>> {
3✔
169
    let parent = match PathBuf::from(filename).parent() {
3✔
170
        Some(p) => p.to_path_buf(),
3✔
171
        None => PathBuf::from("."),
×
172
    };
173
    match std::fs::File::open(filename) {
3✔
174
        Err(e) => Err(MeisterError::IO(e)),
1✔
175
        Ok(file) => read_from_reader(Box::new(std::io::BufReader::new(file)), parent),
2✔
176
    }
177
}
3✔
178

179
fn convert_and_push_item(item: &str, result: &mut Vec<PathBuf>, errs: &mut Vec<MeisterError>) {
11✔
180
    let path = PathBuf::from(item);
11✔
181
    if !path.exists() {
11✔
182
        errs.push(MeisterError::ProjectNotFound(path));
3✔
183
    } else if path.is_file() {
8✔
184
        if btmeister::is_supported_archive_format(&path) {
1✔
185
            result.push(path);
×
186
        } else {
1✔
187
            errs.push(MeisterError::ProjectNotFound(path));
1✔
188
        }
1✔
189
    } else if path.is_dir() {
7✔
190
        result.push(path);
7✔
191
    } else {
7✔
192
        errs.push(MeisterError::ProjectNotFound(path));
×
193
    }
×
194
}
11✔
195

196
fn push_items_or_errs(
3✔
197
    r: Result<Vec<String>>,
3✔
198
    results: &mut Vec<PathBuf>,
3✔
199
    errs: &mut Vec<MeisterError>,
3✔
200
) {
3✔
201
    match r {
3✔
202
        Err(e) => errs.push(e),
1✔
203
        Ok(items) => {
2✔
204
            for item in items {
8✔
205
                convert_and_push_item(&item, results, errs)
6✔
206
            }
207
        }
208
    }
209
}
3✔
210

211
impl InputOpts {
212
    pub(crate) fn projects(&self) -> Result<Vec<PathBuf>> {
8✔
213
        let mut errs = vec![];
8✔
214
        let mut result = vec![];
8✔
215
        for item in self.dirs.iter() {
8✔
216
            if item == "-" {
8✔
217
                push_items_or_errs(read_from_stdin(), &mut result, &mut errs);
×
218
            } else if let Some(stripped) = item.strip_prefix('@') {
8✔
219
                push_items_or_errs(read_from_file(stripped), &mut result, &mut errs);
3✔
220
            } else {
5✔
221
                convert_and_push_item(item.as_str(), &mut result, &mut errs);
5✔
222
            }
5✔
223
        }
224
        if !errs.is_empty() {
8✔
225
            Err(MeisterError::Array(errs))
4✔
226
        } else if result.is_empty() {
4✔
227
            Err(MeisterError::NoProjectSpecified())
1✔
228
        } else {
229
            Ok(result)
3✔
230
        }
231
    }
8✔
232
}
233

234
#[cfg(test)]
235
mod tests {
236
    use super::*;
237

238
    #[test]
239
    fn test_projects1() {
1✔
240
        let opts = Options::parse_from(["meister", "../testdata/fibonacci", "../testdata/hello"]);
1✔
241
        let projects = opts.inputs.projects();
1✔
242
        assert!(projects.is_ok());
1✔
243
        if let Ok(p) = projects {
1✔
244
            assert_eq!(2, p.len());
1✔
245
            assert_eq!(PathBuf::from("../testdata/fibonacci"), p[0]);
1✔
246
            assert_eq!(PathBuf::from("../testdata/hello"), p[1]);
1✔
247
        }
×
248
    }
1✔
249

250
    #[test]
251
    fn test_projects2() {
1✔
252
        let opts = Options::parse_from(["meister", "@../testdata/project_list.txt"]);
1✔
253
        let projects = opts.inputs.projects();
1✔
254
        if let Ok(p) = projects {
1✔
255
            assert_eq!(2, p.len());
1✔
256
            assert_eq!(PathBuf::from("../testdata/hello"), p[0]);
1✔
257
            assert_eq!(PathBuf::from("../testdata/fibonacci"), p[1]);
1✔
258
        } else {
NEW
259
            panic!("fatal: {projects:?}");
×
260
        }
261
    }
1✔
262

263
    #[test]
264
    fn test_not_exist_project() {
1✔
265
        let opts = Options::parse_from(["meister", "not_exist_project"]);
1✔
266
        let projects = opts.inputs.projects();
1✔
267
        assert!(projects.is_err());
1✔
268
        if let Err(MeisterError::Array(e)) = projects {
1✔
269
            assert_eq!(1, e.len());
1✔
270
            if let MeisterError::ProjectNotFound(p) = &e[0] {
1✔
271
                assert_eq!(&PathBuf::from("not_exist_project"), p);
1✔
272
            }
×
273
        }
×
274
    }
1✔
275

276
    #[test]
277
    fn test_invalid_project_list() {
1✔
278
        let opts = Options::parse_from(["meister", "@../testdata/invalid_project_list.txt"]);
1✔
279
        let projects = opts.inputs.projects();
1✔
280
        assert!(projects.is_err());
1✔
281
        if let Err(MeisterError::Array(e)) = projects {
1✔
282
            assert_eq!(2, e.len());
1✔
283
            if let MeisterError::ProjectNotFound(p) = &e[0] {
1✔
284
                assert_eq!(&PathBuf::from("../testdata/not_exist_project"), p);
1✔
285
            }
×
286
            if let MeisterError::ProjectNotFound(p) = &e[1] {
1✔
287
                assert_eq!(&PathBuf::from("../testdata/project_list.txt"), p);
1✔
288
            }
×
289
        }
×
290
    }
1✔
291

292
    #[test]
293
    fn test_unknownfile() {
1✔
294
        let opts = Options::parse_from(["meister", "@unknownfile"]);
1✔
295
        let projects = opts.inputs.projects();
1✔
296
        assert!(projects.is_err());
1✔
297
        if let Err(MeisterError::Array(e)) = projects {
1✔
298
            assert_eq!(1, e.len());
1✔
299
            if let MeisterError::IO(p) = &e[0] {
1✔
300
                assert_eq!(std::io::ErrorKind::NotFound, p.kind());
1✔
301
            }
×
302
        }
×
303
    }
1✔
304

305
    #[test]
306
    fn test_no_projects() {
1✔
307
        let opts = InputOpts {
1✔
308
            ignore_types: vec![],
1✔
309
            skips: vec![],
1✔
310
            dirs: vec![],
1✔
311
        };
1✔
312
        let projects = opts.projects();
1✔
313
        assert!(projects.is_err());
1✔
314
        match projects {
1✔
315
            Err(MeisterError::NoProjectSpecified()) => {}
1✔
NEW
316
            _ => panic!("fatal: {projects:?}"),
×
317
        }
318
    }
1✔
319
}
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

© 2025 Coveralls, Inc