Coverage Report

Created: 2022-09-02 18:59

/Users/oliverbrotchie/Documents/GitHub/csv-ledger/src/main.rs
Line
Count
Source (jump to first uncovered line)
1
1
//! # `csv_ledger`//! # `csv_ledger`
2
//!  Consume a CSV containing a list of transactions and produce a set of client account statements.
3
//!
4
//! ## Installation
5
//!
6
//! ```sh
7
//! cargo install csv_ledger
8
//! ```
9
//!
10
//! ## Usage
11
//!
12
//! **Print output to console:**
13
//! ```sh
14
//! csv_ledger foo.csv
15
//! ```
16
//!
17
//! **Save output to file:**
18
//! ```sh
19
//! csv_ledger --output out.csv foo.csv
20
//! ```
21
//!
22
//! **To see help infomation:**
23
//! ```sh
24
//! csv_ledger --help
25
//! ```
26
//!
27
pub mod ledger;
28
pub mod parse;
29
30
use clap::Parser;
31
use core::fmt;
32
use ledger::Ledger;
33
use nom::Err as NomErr;
34
use std::{
35
    env,
36
    fmt::Display,
37
    fs::{self, File},
38
    io::{self, BufReader},
39
    path::PathBuf,
40
    process::ExitCode,
41
};
42
43
1
#[derive(Debug)]
44
pub enum LedgerErr {
45
    Opening(io::Error),
46
    Reading(io::Error),
47
    Saving(io::Error),
48
    Parse(String, usize),
49
}
50
51
impl LedgerErr {
52
4
    fn from_parse<E>(err: NomErr<E>, index: usize) -> LedgerErr {
53
4
        LedgerErr::Parse(
54
4
            match err {
55
1
                NomErr::Incomplete(_) => "Input was incomplete",
56
2
                NomErr::Error(_) => "Input was in the wrong format",
57
1
                NomErr::Failure(_) => "Faliure whilst parsing input",
58
            }
59
4
            .to_string(),
60
4
            index,
61
4
        )
62
4
    }
<csv_ledger::LedgerErr>::from_parse::<nom::error::Error<&str>>
Line
Count
Source
52
1
    fn from_parse<E>(err: NomErr<E>, index: usize) -> LedgerErr {
53
1
        LedgerErr::Parse(
54
1
            match err {
55
0
                NomErr::Incomplete(_) => "Input was incomplete",
56
1
                NomErr::Error(_) => "Input was in the wrong format",
57
0
                NomErr::Failure(_) => "Faliure whilst parsing input",
58
            }
59
1
            .to_string(),
60
1
            index,
61
1
        )
62
1
    }
<csv_ledger::LedgerErr>::from_parse::<nom::internal::Needed>
Line
Count
Source
52
1
    fn from_parse<E>(err: NomErr<E>, index: usize) -> LedgerErr {
53
1
        LedgerErr::Parse(
54
1
            match err {
55
1
                NomErr::Incomplete(_) => "Input was incomplete",
56
0
                NomErr::Error(_) => "Input was in the wrong format",
57
0
                NomErr::Failure(_) => "Faliure whilst parsing input",
58
            }
59
1
            .to_string(),
60
1
            index,
61
1
        )
62
1
    }
<csv_ledger::LedgerErr>::from_parse::<(&str, nom::error::ErrorKind)>
Line
Count
Source
52
2
    fn from_parse<E>(err: NomErr<E>, index: usize) -> LedgerErr {
53
2
        LedgerErr::Parse(
54
2
            match err {
55
0
                NomErr::Incomplete(_) => "Input was incomplete",
56
1
                NomErr::Error(_) => "Input was in the wrong format",
57
1
                NomErr::Failure(_) => "Faliure whilst parsing input",
58
            }
59
2
            .to_string(),
60
2
            index,
61
2
        )
62
2
    }
63
}
64
65
impl Display for LedgerErr {
66
8
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
67
8
        let (
msg, e4
) = match self {
68
2
            LedgerErr::Opening(e) => ("opening the csv", e),
69
1
            LedgerErr::Reading(e) => ("reading in the csv", e),
70
1
            LedgerErr::Saving(e) => ("saving the output file", e),
71
4
            LedgerErr::Parse(e, index) => {
72
4
                return write!(
73
4
                    f,
74
4
                    "Ledger Error 🦀 - Issue whilst parsing csv: \"{}\", At line: {index}",
75
4
                    e
76
4
                )
77
            }
78
        };
79
80
4
        write!(f, "Ledger Error 🦀 - Issue whilst {msg}: {}", e)
81
8
    }
82
}
83
84
4
#[derive(Parser, 
Debug1
)]
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches
Unexecuted instantiation: <csv_ledger::Args as clap::derive::CommandFactory>::into_app_for_update
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args_for_update
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#3}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#0}
<csv_ledger::Args as clap::derive::Args>::augment_args
Line
Count
Source
84
4
#[derive(Parser, Debug)]
<csv_ledger::Args as clap::derive::CommandFactory>::into_app
Line
Count
Source
84
2
#[derive(Parser, Debug)]
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#3}
85
#[clap(author, version, about)]
86
struct Args {
87
    /// The path to the input CSV File.
88
0
    path: PathBuf,
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#2}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args_for_update::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#2}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args_for_update::{closure#0}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#2}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#2}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args::{closure#0}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#1}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#1}
89
90
    #[clap(short = 'o', long = "output")]
91
    /// A path to save the output a a file. By default, the output will be printed to stdout.
92
0
    output: Option<PathBuf>,
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args_for_update::{closure#1}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#4}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args_for_update::{closure#1}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#4}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args::{closure#1}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#4}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args::{closure#1}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#4}::{closure#0}
93
}
94
95
impl Args {
96
    /// Parse cli args or read mocked test enviroment variables.
97
    /// Whilst this method is ugly, it allows for higher code coverage than using `try_parse` alone.
98
5
    fn parse_input() -> Result<Args, clap::Error> {
99
5
        if cfg!(feature = "test_args") && env::var("CSV_LEDGER_TEST_ARGS").is_ok() {
100
4
            match env::var("CSV_LEDGER_PATH") {
101
3
                Ok(p) => Ok(Args {
102
3
                    path: p.into(),
103
3
                    output: env::var("CSV_LEDGER_OUTPUT").ok().map(|s| 
s.into()1
),
104
3
                }),
105
1
                Err(_) => Err(clap::Error::with_description(
106
1
                    "CSV_LEDGER_PATH environment variable not set.".to_string(),
107
1
                    clap::ErrorKind::MissingRequiredArgument,
108
1
                )),
109
            }
110
        } else {
111
1
            Args::try_parse()
112
        }
113
5
    }
114
}
115
116
5
fn main() -> ExitCode {
117
5
    let 
args3
= match Args::parse_input() {
118
3
        Ok(args) => args,
119
2
        Err(err) => {
120
2
            eprintln!("{err}");
121
2
            return ExitCode::FAILURE;
122
        }
123
    };
124
125
3
    if let Err(
err1
) = perform_parse_and_output(args.path, args.output) {
126
1
        eprintln!("{err}");
127
1
        return ExitCode::FAILURE;
128
2
    }
129
2
130
2
    ExitCode::SUCCESS
131
5
}
132
133
#[inline]
134
/// Run the main functionality of the CLI.
135
8
pub fn perform_parse_and_output(path: PathBuf, output: Option<PathBuf>) -> Result<(), LedgerErr> {
136
    // Open the csv file
137
8
    let 
file6
= File::open(path).map_err(LedgerErr::Opening)
?2
;
138
139
    // Create a new ledger and consume the csv file
140
6
    let mut ledger = Ledger::default();
141
6
    ledger.consume_csv(BufReader::new(file))
?1
;
142
143
    // Output the result
144
5
    if let Some(
output_path3
) = output {
145
3
        fs::write(output_path, ledger.to_string()).map_err(LedgerErr::Saving)
?1
;
146
2
    } else {
147
2
        println!("{}", ledger);
148
2
    }
149
150
4
    Ok(())
151
8
}
152
153
#[cfg(test)]
154
mod perform_parse_and_output {
155
    use std::{fs, path::Path};
156
    use tempfile::tempdir;
157
158
1
    #[test]
159
1
    fn ok_stdout() {
160
1
        let dir = tempdir().expect("Failed to create temporary directory");
161
1
        let path = dir.path().join("test.csv");
162
1
        let input = "type, client, tx, amount\ndeposit, 1, 1, 1.0";
163
1
164
1
        fs::write(&path, input).expect("Failed to create temporary file");
165
1
166
1
        let result = super::perform_parse_and_output(path.clone().into(), None);
167
1
        assert!(result.is_ok());
168
1
    }
169
170
1
    #[test]
171
1
    fn ok_output_file() {
172
1
        let dir = tempdir().expect("Failed to create temporary directory");
173
1
        let path = dir.path().join("test.csv");
174
1
        let output = dir.path().join("test_output.csv");
175
1
        let input = "type, client, tx, amount\ndeposit, 1, 1, 1.0";
176
1
177
1
        fs::write(&path, input).expect("Unable to write file");
178
1
179
1
        let result =
180
1
            super::perform_parse_and_output(path.clone().into(), Some(output.clone().into()));
181
1
182
1
        result.unwrap();
183
1
        assert!(Path::new(&output).is_file());
184
1
    }
185
186
1
    #[test]
187
1
    fn err_read_file() {
188
1
        let dir = tempdir().expect("Failed to create temporary directory");
189
1
        let path = dir.path().join("/foo/test.csv");
190
1
191
1
        let result = super::perform_parse_and_output(path.clone().into(), None);
192
1
        assert!(result.is_err());
193
1
    }
194
195
1
    #[test]
196
1
    fn err_consume() {
197
1
        let dir = tempdir().expect("Failed to create temporary directory");
198
1
        let path = dir.path().join("test.csv");
199
1
        let input = "";
200
1
201
1
        fs::write(&path, input).expect("Failed to create temporary file");
202
1
203
1
        let result = super::perform_parse_and_output(path.clone().into(), None);
204
1
        assert!(result.is_err());
205
1
    }
206
207
1
    #[test]
208
1
    fn err_output_file() {
209
1
        let dir = tempdir().expect("Failed to create temporary directory");
210
1
        let path = dir.path().join("test.csv");
211
1
        let output = dir.path().join("/example/test_output.csv");
212
1
        let input = "type, client, tx, amount\ndeposit, 1, 1, 1.0";
213
1
214
1
        fs::write(&path, input).expect("Unable to write file");
215
1
216
1
        let result =
217
1
            super::perform_parse_and_output(path.clone().into(), Some(output.clone().into()));
218
1
        assert!(result.is_err());
219
1
    }
220
}
221
222
#[cfg(test)]
223
mod args {
224
    use super::Args;
225
    use clap::Parser;
226
227
1
    #[test]
228
1
    fn debug() {
229
1
        let args = Args {
230
1
            path: "./tests/test.csv".into(),
231
1
            output: Some("./tests/test_output.csv".into()),
232
1
        };
233
1
234
1
        assert_eq!(
235
1
            format!("{:?}", args),
236
1
            "Args { path: \"./tests/test.csv\", output: Some(\"./tests/test_output.csv\") }"
237
1
        );
238
1
    }
239
240
1
    #[test]
241
1
    fn parse_err() {
242
1
        Args::try_parse_from(["foo.csv"]).unwrap_err();
243
1
    }
244
}
245
246
#[cfg(test)]
247
mod ledger_err {
248
    use crate::LedgerErr;
249
    use nom::{error::ErrorKind, Err as NomErr, Needed};
250
251
1
    #[test]
252
1
    fn from_parse() {
253
1
        assert_eq!(
254
1
            LedgerErr::from_parse(NomErr::Incomplete::<Needed>(Needed::Unknown), 1).to_string(),
255
1
            "Ledger Error 🦀 - Issue whilst parsing csv: \"Input was incomplete\", At line: 1",
256
1
        );
257
258
1
        assert_eq!(
259
1
            LedgerErr::from_parse(NomErr::Failure(("ERROR", ErrorKind::Fail)), 1).to_string(),
260
1
            "Ledger Error 🦀 - Issue whilst parsing csv: \"Faliure whilst parsing input\", At line: 1",
261
1
        );
262
263
1
        assert_eq!(
264
1
            LedgerErr::from_parse(NomErr::Error(("ERROR", ErrorKind::Fail)), 1).to_string(),
265
1
            "Ledger Error 🦀 - Issue whilst parsing csv: \"Input was in the wrong format\", At line: 1",
266
1
        );
267
1
    }
268
269
1
    #[test]
270
1
    fn debug() {
271
1
        let err = super::LedgerErr::Opening(std::io::Error::new(
272
1
            std::io::ErrorKind::NotFound,
273
1
            "File not found",
274
1
        ));
275
1
        assert_eq!(
276
1
            format!("{:?}", err),
277
1
            "Opening(Custom { kind: NotFound, error: \"File not found\" })",
278
1
        );
279
1
    }
280
281
1
    #[test]
282
1
    fn display() {
283
1
        assert_eq!(
284
1
            format!(
285
1
                "{}",
286
1
                super::LedgerErr::Opening(std::io::Error::new(
287
1
                    std::io::ErrorKind::NotFound,
288
1
                    "File not found",
289
1
                ))
290
1
            ),
291
1
            "Ledger Error 🦀 - Issue whilst opening the csv: File not found",
292
1
        );
293
294
1
        assert_eq!(
295
1
            format!(
296
1
                "{}",
297
1
                super::LedgerErr::Reading(std::io::Error::new(
298
1
                    std::io::ErrorKind::NotFound,
299
1
                    "File not found",
300
1
                ))
301
1
            ),
302
1
            "Ledger Error 🦀 - Issue whilst reading in the csv: File not found",
303
1
        );
304
305
1
        assert_eq!(
306
1
            format!(
307
1
                "{}",
308
1
                super::LedgerErr::Saving(std::io::Error::new(
309
1
                    std::io::ErrorKind::NotFound,
310
1
                    "File not found",
311
1
                ))
312
1
            ),
313
1
            "Ledger Error 🦀 - Issue whilst saving the output file: File not found",
314
1
        );
315
316
1
        assert_eq!(
317
1
            format!("{}", super::LedgerErr::Parse("ERROR".into(), 1)),
318
1
            "Ledger Error 🦀 - Issue whilst parsing csv: \"ERROR\", At line: 1"
319
1
        );
320
1
    }
321
}
322
323
// Needed to up the code coverage of main
324
#[cfg(all(test, feature = "test_args"))]
325
mod main {
326
    use crate::main;
327
    use std::{env, fs};
328
    use tempfile::tempdir;
329
330
5
    fn reset_args() {
331
5
        env::remove_var("CSV_LEDGER_TEST_ARGS");
332
5
        env::remove_var("CSV_LEDGER_OUTPUT");
333
5
        env::remove_var("CSV_LEDGER_PATH");
334
5
    }
335
336
1
    #[test]
337
1
    fn ok_stdout() {
338
1
        reset_args();
339
1
        let dir = tempdir().expect("Failed to create temporary directory");
340
1
        let path = dir.path().join("test.csv");
341
1
        let input = "type, client, tx, amount\ndeposit, 1, 1, 1.0";
342
1
343
1
        fs::write(&path, input).expect("Unable to write file");
344
1
345
1
        env::set_var("CSV_LEDGER_TEST_ARGS", "true");
346
1
        env::set_var("CSV_LEDGER_PATH", path);
347
1
        main();
348
1
    }
349
350
1
    #[test]
351
1
    fn ok_file() {
352
1
        reset_args();
353
1
354
1
        let dir = tempdir().expect("Failed to create temporary directory");
355
1
        let path = dir.path().join("test.csv");
356
1
        let output = dir.path().join("test_output.csv");
357
1
        let input = "type, client, tx, amount\ndeposit, 1, 1, 1.0";
358
1
359
1
        fs::write(&path, input).expect("Unable to write file");
360
1
361
1
        env::set_var("CSV_LEDGER_TEST_ARGS", "true");
362
1
        env::set_var("CSV_LEDGER_PATH", path);
363
1
        env::set_var("CSV_LEDGER_OUTPUT", output);
364
1
        main();
365
1
    }
366
367
1
    #[test]
368
1
    fn err_invalid_path() {
369
1
        reset_args();
370
1
        let dir = tempdir().expect("Failed to create temporary directory");
371
1
        env::set_var("CSV_LEDGER_TEST_ARGS", "true");
372
1
        env::set_var("CSV_LEDGER_PATH", dir.path().join("foo.csv"));
373
1
        main();
374
1
    }
375
376
1
    #[test]
377
1
    fn err_missing_path() {
378
1
        reset_args();
379
1
        env::set_var("CSV_LEDGER_TEST_ARGS", "true");
380
1
        main();
381
1
    }
382
383
1
    #[test]
384
1
    fn err_default_args() {
385
1
        reset_args();
386
1
        main();
387
1
    }
388
}