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

tamada / btmeister / 13833494181

13 Mar 2025 11:26AM UTC coverage: 90.704% (+0.2%) from 90.485%
13833494181

push

github

web-flow
Merge pull request #67 from tamada/separate_lib_and_cli

Separate lib and cli

81 of 95 new or added lines in 6 files covered. (85.26%)

1444 of 1592 relevant lines covered (90.7%)

20.51 hits per line

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

82.98
/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
#[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
    )]
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>, parent: PathBuf) -> Result<Vec<String>> {
2✔
136
    let result = r
2✔
137
        .lines()
2✔
138
        .map_while(|r| r.ok())
14✔
139
        .map(|l| l.trim().to_string())
14✔
140
        .filter(|l| !l.starts_with("#") && !l.is_empty())
14✔
141
        .map(|name| parent.join(name).to_str().unwrap().to_string())
6✔
142
        .collect::<Vec<String>>();
2✔
143
    Ok(result)
2✔
144
}
2✔
145

146
fn read_from_stdin() -> Result<Vec<String>> {
×
NEW
147
    read_from_reader(Box::new(io::stdin().lock()), PathBuf::from("."))
×
148
}
×
149

150
fn read_from_file(filename: &str) -> Result<Vec<String>> {
3✔
151
    let parent = match PathBuf::from(filename).parent() {
3✔
152
        Some(p) => p.to_path_buf(),
3✔
NEW
153
        None => PathBuf::from("."),
×
154
    };
155
    match std::fs::File::open(filename) {
3✔
156
        Err(e) => Err(MeisterError::IO(e)),
1✔
157
        Ok(file) => read_from_reader(Box::new(std::io::BufReader::new(file)), parent),
2✔
158
    }
159
}
3✔
160

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

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

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

216
#[cfg(test)]
217
mod tests {
218
    use super::*;
219

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

232
    #[test]
233
    fn test_projects2() {
1✔
234
        let opts = Options::parse_from(["meister", "@../testdata/project_list.txt"]);
1✔
235
        let projects = opts.inputs.projects();
1✔
236
        if let Ok(p) = projects {
1✔
237
            assert_eq!(2, p.len());
1✔
238
            assert_eq!(PathBuf::from("../testdata/hello"), p[0]);
1✔
239
            assert_eq!(PathBuf::from("../testdata/fibonacci"), p[1]);
1✔
240
        } else {
NEW
241
            panic!("fatal: {:?}", projects);
×
242
        }
243
    }
1✔
244

245
    #[test]
246
    fn test_not_exist_project() {
1✔
247
        let opts = Options::parse_from(["meister", "not_exist_project"]);
1✔
248
        let projects = opts.inputs.projects();
1✔
249
        assert!(projects.is_err());
1✔
250
        if let Err(MeisterError::Array(e)) = projects {
1✔
251
            assert_eq!(1, e.len());
1✔
252
            if let MeisterError::ProjectNotFound(p) = &e[0] {
1✔
253
                assert_eq!(&PathBuf::from("not_exist_project"), p);
1✔
254
            }
×
255
        }
×
256
    }
1✔
257

258
    #[test]
259
    fn test_invalid_project_list() {
1✔
260
        let opts = Options::parse_from(["meister", "@../testdata/invalid_project_list.txt"]);
1✔
261
        let projects = opts.inputs.projects();
1✔
262
        assert!(projects.is_err());
1✔
263
        if let Err(MeisterError::Array(e)) = projects {
1✔
264
            assert_eq!(2, e.len());
1✔
265
            if let MeisterError::ProjectNotFound(p) = &e[0] {
1✔
266
                assert_eq!(&PathBuf::from("../testdata/not_exist_project"), p);
1✔
267
            }
×
268
            if let MeisterError::ProjectNotFound(p) = &e[1] {
1✔
269
                assert_eq!(&PathBuf::from("../testdata/project_list.txt"), p);
1✔
270
            }
×
271
        }
×
272
    }
1✔
273

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

287
    #[test]
288
    fn test_no_projects() {
1✔
289
        let opts = InputOpts {
1✔
290
            ignore_types: vec![],
1✔
291
            excludes: vec![],
1✔
292
            dirs: vec![],
1✔
293
        };
1✔
294
        let projects = opts.projects();
1✔
295
        assert!(projects.is_err());
1✔
296
        match projects {
1✔
297
            Err(MeisterError::NoProjectSpecified()) => {}
1✔
NEW
298
            _ => panic!("fatal: {:?}", projects),
×
299
        }
300
    }
1✔
301
}
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