/Users/oliverbrotchie/Documents/GitHub/csv-ledger/src/ledger.rs
Line | Count | Source |
1 | | //! # Ledger |
2 | | //! This module contains `Ledger`, the state store used for this CLI. |
3 | | //! |
4 | | //! `Ledger` stores running totals of client accounts, consumes csv files and |
5 | | //! outputs associated to account statements to string. |
6 | | //! |
7 | | //! **Basic example:** |
8 | | //! ```rust |
9 | | //! use crate::ledger::ledger |
10 | | //! |
11 | | //! fn main() { |
12 | | //! // Read in a new file |
13 | | //! let reader = BufReader::new(File::open("./foo.csv").unwrap()); |
14 | | //! |
15 | | //! // Create a new ledger and read in the csv file line by line |
16 | | //! let ledger = Ledger::default(); |
17 | | //! ledger.consume_csv(reader); |
18 | | //! |
19 | | //! // Print out the result |
20 | | //! println!("{}", ledger); |
21 | | //! } |
22 | | //! ``` |
23 | | |
24 | | use crate::{ |
25 | | parse::{parse_header, parse_transaction, Transaction}, |
26 | | LedgerErr, |
27 | | }; |
28 | | use std::{ |
29 | | collections::{BTreeMap, HashMap}, |
30 | | fmt::{self, Display}, |
31 | | io::{BufRead, BufReader, Read}, |
32 | | }; |
33 | | |
34 | 13 | #[derive(Default, Debug1 )] |
35 | | pub struct Ledger { |
36 | | /// The list of client accounts. |
37 | | pub clients: HashMap<u16, ClientData>, |
38 | | /// The list of transactions. Note: This is a nieve implementation of transaction storage, |
39 | | /// requiring all transactions to be stored in memory. Due to there being no maximum limmit to |
40 | | /// how old a transaction can be for a `hold` to be applied, all transactions must be addressable. |
41 | | pub transactions: BTreeMap<u32, i64>, |
42 | | } |
43 | | |
44 | | /// An individual client account. |
45 | 1 | #[derive(Debug)] |
46 | | pub struct ClientData { |
47 | | held: BTreeMap<u32, i64>, |
48 | | available: i64, |
49 | | total: i64, |
50 | | locked: bool, |
51 | | } |
52 | | |
53 | | impl Ledger { |
54 | | #[inline] |
55 | | /// Consume a `BufReader` that contains a csv file of transactions. |
56 | | pub fn consume_csv<T>(&mut self, mut reader: BufReader<T>) -> Result<(), LedgerErr> |
57 | | where |
58 | | T: Read, |
59 | | { |
60 | 12 | validate_header(&mut reader)?4 ; |
61 | | |
62 | 18 | for (index, line) in reader.lines().enumerate()8 { |
63 | 18 | let res17 = line.map_err(LedgerErr::Reading)?1 ; // map_err is used to provide better debug info |
64 | 17 | if !res.trim().is_empty() { |
65 | 13 | match parse_transaction(&res) |
66 | 13 | .map_err(|err| LedgerErr::from_parse(err, index + 2)1 )?1 Unexecuted instantiation: <csv_ledger::ledger::Ledger>::consume_csv::<std::io::cursor::Cursor<&[u8; 1usize]>>::{closure#0}<csv_ledger::ledger::Ledger>::consume_csv::<std::io::cursor::Cursor<&str>>::{closure#0}Line | Count | Source | 66 | 1 | .map_err(|err| LedgerErr::from_parse(err, index + 2))? |
Unexecuted instantiation: <csv_ledger::ledger::Ledger>::consume_csv::<csv_ledger::ledger::ledger::TestReader>::{closure#0}Unexecuted instantiation: <csv_ledger::ledger::Ledger>::consume_csv::<csv_ledger::ledger::ledger::TestReaderTwo>::{closure#0}Unexecuted instantiation: <csv_ledger::ledger::Ledger>::consume_csv::<std::fs::File>::{closure#0} |
67 | | { |
68 | 1 | Transaction::Withdrawal(id, tx, amount) => { |
69 | 1 | self.insert_transaction(id, tx, -amount) // Negative amounts for withdrawals |
70 | | } |
71 | 7 | Transaction::Deposit(id, tx, amount) => self.insert_transaction(id, tx, amount), |
72 | 2 | Transaction::Dispute(id, tx) => self.hold(id, tx), |
73 | 1 | Transaction::Resolve(id, tx) => self.resolve(id, tx), |
74 | 1 | Transaction::Chargeback(id, tx) => self.chageback(id, tx), |
75 | | } |
76 | 4 | } |
77 | | } |
78 | | |
79 | 6 | Ok(()) |
80 | 12 | } <csv_ledger::ledger::Ledger>::consume_csv::<csv_ledger::ledger::ledger::TestReader> Line | Count | Source | 60 | 1 | validate_header(&mut reader)?; | 61 | | | 62 | 0 | for (index, line) in reader.lines().enumerate() { | 63 | 0 | let res = line.map_err(LedgerErr::Reading)?; // map_err is used to provide better debug info | 64 | 0 | if !res.trim().is_empty() { | 65 | 0 | match parse_transaction(&res) | 66 | 0 | .map_err(|err| LedgerErr::from_parse(err, index + 2))? | 67 | | { | 68 | 0 | Transaction::Withdrawal(id, tx, amount) => { | 69 | 0 | self.insert_transaction(id, tx, -amount) // Negative amounts for withdrawals | 70 | | } | 71 | 0 | Transaction::Deposit(id, tx, amount) => self.insert_transaction(id, tx, amount), | 72 | 0 | Transaction::Dispute(id, tx) => self.hold(id, tx), | 73 | 0 | Transaction::Resolve(id, tx) => self.resolve(id, tx), | 74 | 0 | Transaction::Chargeback(id, tx) => self.chageback(id, tx), | 75 | | } | 76 | 0 | } | 77 | | } | 78 | | | 79 | 0 | Ok(()) | 80 | 1 | } |
<csv_ledger::ledger::Ledger>::consume_csv::<std::io::cursor::Cursor<&str>> Line | Count | Source | 60 | 3 | validate_header(&mut reader)?1 ; | 61 | | | 62 | 12 | for (index, line) in reader.lines().enumerate()2 { | 63 | 12 | let res = line.map_err(LedgerErr::Reading)?0 ; // map_err is used to provide better debug info | 64 | 12 | if !res.trim().is_empty() { | 65 | 8 | match parse_transaction(&res) | 66 | 8 | .map_err(|err| LedgerErr::from_parse(err, index + 2))?1 | 67 | | { | 68 | 1 | Transaction::Withdrawal(id, tx, amount) => { | 69 | 1 | self.insert_transaction(id, tx, -amount) // Negative amounts for withdrawals | 70 | | } | 71 | 2 | Transaction::Deposit(id, tx, amount) => self.insert_transaction(id, tx, amount), | 72 | 2 | Transaction::Dispute(id, tx) => self.hold(id, tx), | 73 | 1 | Transaction::Resolve(id, tx) => self.resolve(id, tx), | 74 | 1 | Transaction::Chargeback(id, tx) => self.chageback(id, tx), | 75 | | } | 76 | 4 | } | 77 | | } | 78 | | | 79 | 1 | Ok(()) | 80 | 3 | } |
<csv_ledger::ledger::Ledger>::consume_csv::<csv_ledger::ledger::ledger::TestReaderTwo> Line | Count | Source | 60 | 1 | validate_header(&mut reader)?0 ; | 61 | | | 62 | 1 | for (index, line) in reader.lines().enumerate() { | 63 | 1 | let res0 = line.map_err(LedgerErr::Reading)?; // map_err is used to provide better debug info | 64 | 0 | if !res.trim().is_empty() { | 65 | 0 | match parse_transaction(&res) | 66 | 0 | .map_err(|err| LedgerErr::from_parse(err, index + 2))? | 67 | | { | 68 | 0 | Transaction::Withdrawal(id, tx, amount) => { | 69 | 0 | self.insert_transaction(id, tx, -amount) // Negative amounts for withdrawals | 70 | | } | 71 | 0 | Transaction::Deposit(id, tx, amount) => self.insert_transaction(id, tx, amount), | 72 | 0 | Transaction::Dispute(id, tx) => self.hold(id, tx), | 73 | 0 | Transaction::Resolve(id, tx) => self.resolve(id, tx), | 74 | 0 | Transaction::Chargeback(id, tx) => self.chageback(id, tx), | 75 | | } | 76 | 0 | } | 77 | | } | 78 | | | 79 | 0 | Ok(()) | 80 | 1 | } |
<csv_ledger::ledger::Ledger>::consume_csv::<std::fs::File> Line | Count | Source | 60 | 6 | validate_header(&mut reader)?1 ; | 61 | | | 62 | 5 | for (index, line) in reader.lines().enumerate() { | 63 | 5 | let res = line.map_err(LedgerErr::Reading)?0 ; // map_err is used to provide better debug info | 64 | 5 | if !res.trim().is_empty() { | 65 | 5 | match parse_transaction(&res) | 66 | 5 | .map_err(|err| LedgerErr::from_parse(err, index + 2))?0 | 67 | | { | 68 | 0 | Transaction::Withdrawal(id, tx, amount) => { | 69 | 0 | self.insert_transaction(id, tx, -amount) // Negative amounts for withdrawals | 70 | | } | 71 | 5 | Transaction::Deposit(id, tx, amount) => self.insert_transaction(id, tx, amount), | 72 | 0 | Transaction::Dispute(id, tx) => self.hold(id, tx), | 73 | 0 | Transaction::Resolve(id, tx) => self.resolve(id, tx), | 74 | 0 | Transaction::Chargeback(id, tx) => self.chageback(id, tx), | 75 | | } | 76 | 0 | } | 77 | | } | 78 | | | 79 | 5 | Ok(()) | 80 | 6 | } |
<csv_ledger::ledger::Ledger>::consume_csv::<std::io::cursor::Cursor<&[u8; 1usize]>> Line | Count | Source | 60 | 1 | validate_header(&mut reader)?; | 61 | | | 62 | 0 | for (index, line) in reader.lines().enumerate() { | 63 | 0 | let res = line.map_err(LedgerErr::Reading)?; // map_err is used to provide better debug info | 64 | 0 | if !res.trim().is_empty() { | 65 | 0 | match parse_transaction(&res) | 66 | 0 | .map_err(|err| LedgerErr::from_parse(err, index + 2))? | 67 | | { | 68 | 0 | Transaction::Withdrawal(id, tx, amount) => { | 69 | 0 | self.insert_transaction(id, tx, -amount) // Negative amounts for withdrawals | 70 | | } | 71 | 0 | Transaction::Deposit(id, tx, amount) => self.insert_transaction(id, tx, amount), | 72 | 0 | Transaction::Dispute(id, tx) => self.hold(id, tx), | 73 | 0 | Transaction::Resolve(id, tx) => self.resolve(id, tx), | 74 | 0 | Transaction::Chargeback(id, tx) => self.chageback(id, tx), | 75 | | } | 76 | 0 | } | 77 | | } | 78 | | | 79 | 0 | Ok(()) | 80 | 1 | } |
|
81 | | |
82 | | /// Insert a new transaction |
83 | | /// |
84 | | /// Example: |
85 | | /// ```rust |
86 | | /// const ledger = Ledger::default(); |
87 | | /// |
88 | | /// // Deposit |
89 | | /// ledger.insert_transaction(1,1,10.0); |
90 | | /// |
91 | | /// // Withdrawal |
92 | | /// ledger.insert_transaction(1,2,-10.0); |
93 | | /// ``` |
94 | | pub fn insert_transaction(&mut self, client_id: u16, transaction_id: u32, amount: i64) { |
95 | 15 | if let Some(client3 ) = self.clients.get_mut(&client_id) { |
96 | 3 | if !client.locked { |
97 | 2 | client.total += amount; |
98 | 2 | client.available += amount; |
99 | 2 | self.transactions.insert(transaction_id, amount); |
100 | 2 | }1 |
101 | 12 | } else { |
102 | 12 | self.clients.insert(client_id, ClientData::new(amount)); |
103 | 12 | self.transactions.insert(transaction_id, amount); |
104 | 12 | } |
105 | 15 | } |
106 | | |
107 | | /// Opens a dispute on a transaction. |
108 | | pub fn hold(&mut self, client_id: u16, transaction_id: u32) { |
109 | | // Discard any incorrect inputs |
110 | 7 | if let Some(client6 ) = self.clients.get_mut(&client_id) { |
111 | 6 | if let Some(amount5 ) = self.transactions.remove(&transaction_id) { |
112 | 5 | { |
113 | 5 | client.available -= amount; |
114 | 5 | client.held.insert(transaction_id, amount); |
115 | 5 | } |
116 | 1 | } |
117 | 1 | } |
118 | 7 | } |
119 | | |
120 | | /// Resolves a disputed transaction - adds disputed transaction's value back to the available funds. |
121 | | pub fn resolve(&mut self, client_id: u16, transaction_id: u32) { |
122 | | // Discard any incorrect inputs |
123 | 4 | if let Some(client3 ) = self.clients.get_mut(&client_id) { |
124 | 3 | if let Some(amount2 ) = client.held.remove(&transaction_id) { |
125 | 2 | client.available += amount; |
126 | 2 | }1 |
127 | 1 | } |
128 | 4 | } |
129 | | |
130 | | /// Peform a chargeback on a disputed transaction - |
131 | | pub fn chageback(&mut self, client_id: u16, transaction_id: u32) { |
132 | | // Discard any incorrect inputs |
133 | 4 | if let Some(client3 ) = self.clients.get_mut(&client_id) { |
134 | 3 | if let Some(amount2 ) = client.held.remove(&transaction_id) { |
135 | 2 | client.total -= amount; |
136 | 2 | client.locked = true; |
137 | 2 | }1 |
138 | 1 | } |
139 | 4 | } |
140 | | } |
141 | | |
142 | | impl Display for Ledger { |
143 | 7 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
144 | 7 | write!( |
145 | 7 | f, |
146 | 7 | "client, available, held, total, locked{}", |
147 | 7 | self.clients |
148 | 7 | .iter() |
149 | 8 | .fold(String::new(), |acc, (key, value)| format!( |
150 | 8 | "{acc}\n{key}, {value}" |
151 | 8 | )) |
152 | 7 | ) |
153 | 7 | } |
154 | | } |
155 | | |
156 | | /// Validate the header of the csv file. |
157 | 17 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> |
158 | 17 | where |
159 | 17 | T: Read, |
160 | 17 | { |
161 | 17 | let mut buf = String::new(); |
162 | 17 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?2 ; // map_err is used to provide better debug info |
163 | 15 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1)6 )?6 ; Unexecuted instantiation: csv_ledger::ledger::validate_header::<csv_ledger::ledger::ledger::TestReader>::{closure#0}Unexecuted instantiation: csv_ledger::ledger::validate_header::<csv_ledger::ledger::ledger::TestReaderTwo>::{closure#0}Unexecuted instantiation: csv_ledger::ledger::validate_header::<csv_ledger::ledger::validate_header::TestReader>::{closure#0}csv_ledger::ledger::validate_header::<std::io::cursor::Cursor<&str>>::{closure#0}Line | Count | Source | 163 | 4 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?; |
csv_ledger::ledger::validate_header::<std::io::cursor::Cursor<&[u8; 1usize]>>::{closure#0}Line | Count | Source | 163 | 1 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?; |
csv_ledger::ledger::validate_header::<std::fs::File>::{closure#0}Line | Count | Source | 163 | 1 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?; |
|
164 | 9 | Ok(()) |
165 | 17 | } csv_ledger::ledger::validate_header::<csv_ledger::ledger::ledger::TestReader> Line | Count | Source | 157 | 1 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> | 158 | 1 | where | 159 | 1 | T: Read, | 160 | 1 | { | 161 | 1 | let mut buf = String::new(); | 162 | 1 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?; // map_err is used to provide better debug info | 163 | 0 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?; | 164 | 0 | Ok(()) | 165 | 1 | } |
csv_ledger::ledger::validate_header::<std::fs::File> Line | Count | Source | 157 | 6 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> | 158 | 6 | where | 159 | 6 | T: Read, | 160 | 6 | { | 161 | 6 | let mut buf = String::new(); | 162 | 6 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?0 ; // map_err is used to provide better debug info | 163 | 6 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?1 ; | 164 | 5 | Ok(()) | 165 | 6 | } |
csv_ledger::ledger::validate_header::<csv_ledger::ledger::ledger::TestReaderTwo> Line | Count | Source | 157 | 1 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> | 158 | 1 | where | 159 | 1 | T: Read, | 160 | 1 | { | 161 | 1 | let mut buf = String::new(); | 162 | 1 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?0 ; // map_err is used to provide better debug info | 163 | 1 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?0 ; | 164 | 1 | Ok(()) | 165 | 1 | } |
csv_ledger::ledger::validate_header::<csv_ledger::ledger::validate_header::TestReader> Line | Count | Source | 157 | 1 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> | 158 | 1 | where | 159 | 1 | T: Read, | 160 | 1 | { | 161 | 1 | let mut buf = String::new(); | 162 | 1 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?; // map_err is used to provide better debug info | 163 | 0 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?; | 164 | 0 | Ok(()) | 165 | 1 | } |
csv_ledger::ledger::validate_header::<std::io::cursor::Cursor<&[u8; 1usize]>> Line | Count | Source | 157 | 1 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> | 158 | 1 | where | 159 | 1 | T: Read, | 160 | 1 | { | 161 | 1 | let mut buf = String::new(); | 162 | 1 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?0 ; // map_err is used to provide better debug info | 163 | 1 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?; | 164 | 0 | Ok(()) | 165 | 1 | } |
csv_ledger::ledger::validate_header::<std::io::cursor::Cursor<&str>> Line | Count | Source | 157 | 7 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> | 158 | 7 | where | 159 | 7 | T: Read, | 160 | 7 | { | 161 | 7 | let mut buf = String::new(); | 162 | 7 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?0 ; // map_err is used to provide better debug info | 163 | 7 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?4 ; | 164 | 3 | Ok(()) | 165 | 7 | } |
|
166 | | |
167 | | impl ClientData { |
168 | 14 | fn new(amount: i64) -> Self { |
169 | 14 | ClientData { |
170 | 14 | held: BTreeMap::new(), |
171 | 14 | available: amount, |
172 | 14 | total: amount, |
173 | 14 | locked: false, |
174 | 14 | } |
175 | 14 | } |
176 | | } |
177 | | |
178 | | impl Display for ClientData { |
179 | 8 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
180 | 8 | write!( |
181 | 8 | f, |
182 | 8 | "{}, {}, {}, {}", |
183 | 8 | dp_string(self.available), |
184 | 8 | dp_string(self.held.values().sum()), |
185 | 8 | dp_string(self.total), |
186 | 8 | self.locked |
187 | 8 | ) |
188 | 8 | } |
189 | | } |
190 | | |
191 | | /// Convert a i64 to a string with four decimal places (eg val / 100) |
192 | 30 | fn dp_string(amount: i64) -> String { |
193 | 30 | format!("{}.{:04}", amount / 10000, amount % 10000) |
194 | 30 | } |
195 | | |
196 | | #[cfg(test)] |
197 | | mod dp_string { |
198 | | use super::dp_string; |
199 | 1 | #[test] |
200 | 1 | fn test_dp_string() { |
201 | 1 | assert_eq!(dp_string(0), "0.0000"); |
202 | 1 | assert_eq!(dp_string(1), "0.0001"); |
203 | 1 | assert_eq!(dp_string(10), "0.0010"); |
204 | 1 | assert_eq!(dp_string(100), "0.0100"); |
205 | 1 | assert_eq!(dp_string(1000), "0.1000"); |
206 | 1 | assert_eq!(dp_string(10000), "1.0000"); |
207 | 1 | } |
208 | | } |
209 | | |
210 | | #[cfg(test)] |
211 | | mod validate_header { |
212 | | use super::validate_header; |
213 | | use std::io::{BufReader, Cursor, Error, ErrorKind, Read}; |
214 | | |
215 | | struct TestReader {} |
216 | | |
217 | | impl Read for TestReader { |
218 | 1 | fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> { |
219 | 1 | Err(Error::new(ErrorKind::InvalidData, "Something went wrong.")) |
220 | 1 | } |
221 | | } |
222 | | |
223 | 1 | #[test] |
224 | 1 | fn ok() { |
225 | 1 | validate_header(&mut BufReader::new(Cursor::new("type, client, tx, amount"))).unwrap(); |
226 | 1 | } |
227 | | |
228 | 1 | #[test] |
229 | 1 | fn err_runthrough() { |
230 | 1 | validate_header(&mut BufReader::new(TestReader {})).unwrap_err(); |
231 | 1 | validate_header(&mut BufReader::new(Cursor::new(""))).unwrap_err(); |
232 | 1 | validate_header(&mut BufReader::new(Cursor::new("\n"))).unwrap_err(); |
233 | 1 | validate_header(&mut BufReader::new(Cursor::new("type,"))).unwrap_err(); |
234 | 1 | } |
235 | | } |
236 | | |
237 | | #[cfg(test)] |
238 | | mod client_data { |
239 | | use super::ClientData; |
240 | | |
241 | 1 | #[test] |
242 | 1 | fn debug() { |
243 | 1 | let data = ClientData::new(10); |
244 | 1 | |
245 | 1 | assert_eq!( |
246 | 1 | format!("{:?}", data), |
247 | 1 | "ClientData { held: {}, available: 10, total: 10, locked: false }" |
248 | 1 | ); |
249 | 1 | } |
250 | | } |
251 | | |
252 | | #[cfg(test)] |
253 | | mod ledger { |
254 | | use super::{ClientData, Ledger}; |
255 | | use std::collections::BTreeMap; |
256 | | use std::io::{BufReader, Cursor, Error, ErrorKind, Read}; |
257 | | |
258 | | struct TestReader {} |
259 | | |
260 | | impl Read for TestReader { |
261 | 1 | fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> { |
262 | 1 | Err(Error::new(ErrorKind::InvalidData, "Something went wrong.")) |
263 | 1 | } |
264 | | } |
265 | | |
266 | | struct TestReaderTwo<'a> { |
267 | | inner: Cursor<&'a str>, |
268 | | state: bool, |
269 | | } |
270 | | |
271 | | // Fail after second read |
272 | | impl Read for TestReaderTwo<'_> { |
273 | 2 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { |
274 | 2 | if self.state { |
275 | 1 | Err(Error::new(ErrorKind::InvalidData, "Something went wrong.")) |
276 | | } else { |
277 | 1 | self.state = true; |
278 | 1 | Ok(self.inner.read(buf).unwrap()) |
279 | | } |
280 | 2 | } |
281 | | } |
282 | | |
283 | 1 | #[test] |
284 | 1 | fn ok_consume() { |
285 | 1 | let mut ledger = Ledger::default(); |
286 | 1 | |
287 | 1 | ledger |
288 | 1 | .consume_csv(BufReader::new(Cursor::new( |
289 | 1 | "type, client, tx, amount |
290 | 1 | |
291 | 1 | deposit, 1, 1, 20.0 |
292 | 1 | withdrawal,1,2,10.0 |
293 | 1 | dispute,1,2, |
294 | 1 | resolve,1,2, |
295 | 1 | |
296 | 1 | deposit,2,3,113.1112 |
297 | 1 | dispute,2,3, |
298 | 1 | chargeback,2,3, |
299 | 1 | |
300 | 1 | ", |
301 | 1 | ))) |
302 | 1 | .unwrap(); |
303 | 1 | |
304 | 1 | let result = ledger.to_string(); |
305 | 1 | let mut lines = result.lines(); |
306 | 1 | |
307 | 1 | assert_eq!( |
308 | 1 | lines.next().unwrap(), |
309 | 1 | "client, available, held, total, locked" |
310 | 1 | ); |
311 | | |
312 | 1 | let accounts = vec![ |
313 | 1 | "1, 10.0000, 0.0000, 10.0000, false", |
314 | 1 | "2, 0.0000, 0.0000, 0.0000, true", |
315 | 1 | ]; |
316 | 1 | |
317 | 1 | assert!(accounts.contains(&lines.next().unwrap())); |
318 | 1 | assert!(accounts.contains(&lines.next().unwrap())); |
319 | 1 | assert!(lines.next().is_none()) |
320 | 1 | } |
321 | | |
322 | 1 | #[test] |
323 | 1 | fn err_consume_runthrough() { |
324 | 1 | let mut ledger = Ledger::default(); |
325 | 1 | |
326 | 1 | ledger |
327 | 1 | .consume_csv(BufReader::new(Cursor::new(""))) |
328 | 1 | .unwrap_err(); |
329 | 1 | |
330 | 1 | ledger |
331 | 1 | .consume_csv(BufReader::new(Cursor::new(&[0x0]))) |
332 | 1 | .unwrap_err(); |
333 | 1 | |
334 | 1 | ledger |
335 | 1 | .consume_csv(BufReader::new(TestReader {})) |
336 | 1 | .unwrap_err(); |
337 | 1 | |
338 | 1 | ledger |
339 | 1 | .consume_csv(BufReader::new(TestReaderTwo { |
340 | 1 | inner: Cursor::new("type, client, tx, amount\n"), |
341 | 1 | state: false, |
342 | 1 | })) |
343 | 1 | .unwrap_err(); |
344 | 1 | |
345 | 1 | ledger |
346 | 1 | .consume_csv(BufReader::new(Cursor::new("type, client, tx, amount\n123"))) |
347 | 1 | .unwrap_err(); |
348 | 1 | } |
349 | | |
350 | 1 | #[test] |
351 | 1 | fn insert_transaction() { |
352 | 1 | let mut client_2 = ClientData::new(0); |
353 | 1 | client_2.locked = true; |
354 | 1 | |
355 | 1 | let mut ledger = Ledger { |
356 | 1 | clients: [(2_u16, client_2)].into_iter().collect(), |
357 | 1 | transactions: BTreeMap::new(), |
358 | 1 | }; |
359 | 1 | |
360 | 1 | ledger.insert_transaction(1, 1, 1); |
361 | 1 | ledger.insert_transaction(1, 2, 1); |
362 | 1 | |
363 | 1 | // Locked |
364 | 1 | ledger.insert_transaction(2, 3, 1); |
365 | 1 | |
366 | 1 | let client_1 = ledger.clients.get(&1).unwrap(); |
367 | 1 | let client_2 = ledger.clients.get(&2).unwrap(); |
368 | 1 | assert_eq!(client_1.available, 2); |
369 | 1 | assert_eq!(client_2.available, 0); |
370 | 1 | assert_eq!(client_1.total, 2); |
371 | 1 | assert_eq!(client_2.total, 0); |
372 | 1 | } |
373 | | |
374 | 1 | #[test] |
375 | 1 | fn dispute() { |
376 | 1 | let mut ledger = Ledger::default(); |
377 | 1 | |
378 | 1 | ledger.insert_transaction(1, 1, 1); |
379 | 1 | ledger.hold(1, 1); |
380 | 1 | ledger.hold(2, 1); |
381 | 1 | ledger.hold(1, 2); |
382 | 1 | |
383 | 1 | let c = ledger.clients.get(&1).unwrap(); |
384 | 1 | |
385 | 1 | assert_eq!(ledger.clients.len(), 1); |
386 | 1 | assert_eq!(c.held.get(&1).unwrap(), &1_i64); |
387 | 1 | assert_eq!(c.available, 0_i64); |
388 | 1 | } |
389 | | |
390 | 1 | #[test] |
391 | 1 | fn resolve() { |
392 | 1 | let mut ledger = Ledger::default(); |
393 | 1 | |
394 | 1 | ledger.insert_transaction(1, 1, 1); |
395 | 1 | ledger.hold(1, 1); |
396 | 1 | ledger.resolve(1, 1); |
397 | 1 | ledger.resolve(2, 1); |
398 | 1 | ledger.resolve(1, 2); |
399 | 1 | |
400 | 1 | let c = ledger.clients.get(&1).unwrap(); |
401 | 1 | assert_eq!(c.held.len(), 0); |
402 | 1 | assert_eq!(c.available, 1_i64); |
403 | 1 | } |
404 | | |
405 | 1 | #[test] |
406 | 1 | fn chargeback() { |
407 | 1 | let mut ledger = Ledger::default(); |
408 | 1 | |
409 | 1 | ledger.insert_transaction(1, 1, 1); |
410 | 1 | ledger.hold(1, 1); |
411 | 1 | ledger.chageback(1, 1); |
412 | 1 | ledger.chageback(2, 1); |
413 | 1 | ledger.chageback(1, 2); |
414 | 1 | |
415 | 1 | let c = ledger.clients.get(&1).unwrap(); |
416 | 1 | assert_eq!(c.held.len(), 0); |
417 | 1 | assert_eq!(c.total, 0_i64); |
418 | 1 | assert_eq!(c.locked, true); |
419 | 1 | } |
420 | | |
421 | 1 | #[test] |
422 | 1 | fn debug() { |
423 | 1 | assert_eq!( |
424 | 1 | format!("{:?}", Ledger::default()), |
425 | 1 | "Ledger { clients: {}, transactions: {} }" |
426 | 1 | ) |
427 | 1 | } |
428 | | |
429 | 1 | #[test] |
430 | 1 | fn display() { |
431 | 1 | let mut ledger = Ledger::default(); |
432 | 1 | ledger.insert_transaction(1, 1, 1); |
433 | 1 | assert_eq!( |
434 | 1 | format!("{}", ledger), |
435 | 1 | "client, available, held, total, locked\n1, 0.0001, 0.0000, 0.0001, false" |
436 | 1 | ); |
437 | 1 | } |
438 | | } |