main
1
2import 'dart:io';
3
4import 'package:blood_pressure_app/model/export_import/column.dart';
5import 'package:blood_pressure_app/model/export_import/csv_converter.dart';
6import 'package:blood_pressure_app/model/export_import/export_configuration.dart';
7import 'package:blood_pressure_app/model/export_import/record_parsing_result.dart';
8import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
9import 'package:blood_pressure_app/model/storage/export_csv_settings_store.dart';
10import 'package:flutter/material.dart';
11import 'package:flutter_test/flutter_test.dart';
12import 'package:health_data_store/health_data_store.dart';
13
14import 'record_formatter_test.dart';
15
16void main() {
17 test('should create csv string bigger than 0', () {
18 final converter = CsvConverter(CsvExportSettings(), ExportColumnsManager(), []);
19 final csv = converter.create(createRecords());
20 expect(csv.length, isNonZero);
21 });
22
23 test('should create first line', () {
24 final converter = CsvConverter(CsvExportSettings(), ExportColumnsManager(), []);
25 final csv = converter.create([]);
26 final columns = CsvExportSettings().exportFieldsConfiguration.getActiveColumns(ExportColumnsManager());
27 expect(csv, stringContainsInOrder(columns.map((e) => e.csvTitle).toList()));
28 });
29
30 test('should not create first line when setting is off', () {
31 final converter = CsvConverter(
32 CsvExportSettings(exportHeadline: false),
33 ExportColumnsManager(),
34 [],
35 );
36 final csv = converter.create([]);
37 final columns = CsvExportSettings().exportFieldsConfiguration.getActiveColumns(ExportColumnsManager());
38 expect(csv, isNot(stringContainsInOrder(columns.map((e) => e.csvTitle).toList())));
39 });
40
41 test('should be able to recreate records from csv in default configuration', () {
42 final converter = CsvConverter(CsvExportSettings(), ExportColumnsManager(), []);
43 final List<(DateTime, BloodPressureRecord, Note, List<MedicineIntake>, Weight?)> initialRecords = createRecords();
44 final csv = converter.create(initialRecords);
45 final List<FullEntry> parsedRecords = converter.parse(csv).getOr(failParse);
46
47 expect(parsedRecords, pairwiseCompare(initialRecords,
48 ((DateTime, BloodPressureRecord, Note, List<MedicineIntake>, Weight?) p0, FullEntry p1) =>
49 p0.$2.time == p1.$2.time &&
50 p0.$2.sys == p1.$1.sys &&
51 p0.$2.dia == p1.$1.dia &&
52 p0.$2.pul == p1.$1.pul &&
53 p0.$3.note == p1.$2.note &&
54 p0.$3.color == p1.$2.color,
55 'equal to',),);
56 });
57 test('should allow partial imports', () {
58 final text = File('test/model/export_import/exported_formats/incomplete_export.csv').readAsStringSync();
59
60 final converter = CsvConverter(
61 CsvExportSettings(),
62 ExportColumnsManager(),
63 [],
64 );
65 final parsed = converter.parse(text);
66 final records = parsed.getOr(failParse);
67 expect(records, isNotNull);
68 expect(records.length, 3);
69 expect(records, anyElement(isA<FullEntry>()
70 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703239921194)
71 .having((p0) => p0.$1.sys?.mmHg, 'systolic', null)
72 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', null)
73 .having((p0) => p0.$1.pul, 'pulse', null)
74 .having((p0) => p0.$2.note, 'notes', 'note')
75 .having((p0) => p0.$2.color, 'pin', null),
76 ),);
77 expect(records, anyElement(isA<FullEntry>()
78 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703239908244)
79 .having((p0) => p0.$1.sys?.mmHg, 'systolic', null)
80 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 45)
81 .having((p0) => p0.$1.pul, 'pulse', null)
82 .having((p0) => p0.$2.note, 'notes', 'test')
83 .having((p0) => p0.$2.color, 'pin', null),
84 ),);
85 expect(records, anyElement(isA<FullEntry>()
86 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703239905395)
87 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 123)
88 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', null)
89 .having((p0) => p0.$1.pul, 'pulse', null)
90 .having((p0) => p0.$2.note, 'notes', '')
91 .having((p0) => p0.$2.color, 'pin', null),
92 ),);
93 });
94
95
96 test('should import v1.0.0 measurements', () {
97 final text = File('test/model/export_import/exported_formats/v1.0.csv').readAsStringSync();
98
99 final converter = CsvConverter(
100 CsvExportSettings(),
101 ExportColumnsManager(),
102 [],
103 );
104 final parsed = converter.parse(text);
105 final records = parsed.getOr(failParse);
106 expect(records, isNotNull);
107 expect(records.length, 2);
108 expect(records, everyElement(isA<FullEntry>()));
109 expect(records, anyElement(isA<FullEntry>()
110 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703175660000)
111 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 312)
112 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 315)
113 .having((p0) => p0.$1.pul, 'pulse', 46)
114 .having((p0) => p0.$2.note?.trim(), 'notes', 'testfkajkfb'),
115 ),);
116 expect(records, anyElement(isA<FullEntry>()
117 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703175600000)
118 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 123)
119 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 41)
120 .having((p0) => p0.$1.pul, 'pulse', 43)
121 .having((p0) => p0.$2.note?.trim(), 'notes', '1214s3'),
122 ),);
123 });
124 test('should import v1.1.0 measurements', () {
125 final text = File('test/model/export_import/exported_formats/v1.1.0').readAsStringSync();
126
127 final converter = CsvConverter(
128 CsvExportSettings(),
129 ExportColumnsManager(),
130 [],
131 );
132 final parsed = converter.parse(text);
133 final records = parsed.getOr(failParse);
134 expect(records, isNotNull);
135 expect(records.length, 4);
136 expect(records, everyElement(isA<FullEntry>()));
137 expect(records, anyElement(isA<FullEntry>()
138 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703175660000)
139 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 312)
140 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 315)
141 .having((p0) => p0.$1.pul, 'pulse', 46)
142 .having((p0) => p0.$2.note?.trim(), 'notes', 'testfkajkfb'),
143 ),);
144 expect(records, anyElement(isA<FullEntry>()
145 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703175600000)
146 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 123)
147 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 41)
148 .having((p0) => p0.$1.pul, 'pulse', 43)
149 .having((p0) => p0.$2.note?.trim(), 'notes', '1214s3'),
150 ),);
151 });
152 test('should import v1.4.0 measurements', () {
153 final text = File('test/model/export_import/exported_formats/v1.4.0.CSV').readAsStringSync();
154
155 final converter = CsvConverter(
156 CsvExportSettings(),
157 ExportColumnsManager(),
158 [],
159 );
160 final parsed = converter.parse(text);
161 final records = parsed.getOr(failParse);
162 expect(records, isNotNull);
163 expect(records.length, 186);
164 expect(records, everyElement(isA<FullEntry>()));
165 expect(records, anyElement(isA<FullEntry>()
166 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703175660000)
167 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 312)
168 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 315)
169 .having((p0) => p0.$1.pul, 'pulse', 46)
170 .having((p0) => p0.$2.note, 'notes', 'testfkajkfb'),
171 ),);
172 expect(records, anyElement(isA<FullEntry>()
173 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703175600000)
174 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 123)
175 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 41)
176 .having((p0) => p0.$1.pul, 'pulse', 43)
177 .having((p0) => p0.$2.note, 'notes', '1214s3'),
178 ),);
179 expect(records, anyElement(isA<FullEntry>()
180 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 10893142303200)
181 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 106)
182 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 77)
183 .having((p0) => p0.$1.pul, 'pulse', 53)
184 .having((p0) => p0.$2.note, 'notes', ''),
185 ),);
186 });
187 test('should import v1.5.1 measurements', () {
188 final text = File('test/model/export_import/exported_formats/v1.5.1.csv').readAsStringSync();
189
190 final converter = CsvConverter(
191 CsvExportSettings(),
192 ExportColumnsManager(),
193 [],
194 );
195 final parsed = converter.parse(text);
196 final records = parsed.getOr(failParse);
197 expect(records, isNotNull);
198 expect(records.length, 185);
199 expect(records, everyElement(isA<FullEntry>()));
200 expect(records, anyElement(isA<FullEntry>()
201 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703175660000)
202 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 312)
203 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 315)
204 .having((p0) => p0.$1.pul, 'pulse', 46)
205 .having((p0) => p0.$2.note, 'notes', 'testfkajkfb')
206 .having((p0) => p0.$2.color, 'pin', null),
207 ),);
208 expect(records, anyElement(isA<FullEntry>()
209 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703175600000)
210 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 123)
211 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 41)
212 .having((p0) => p0.$1.pul, 'pulse', 43)
213 .having((p0) => p0.$2.note, 'notes', '1214s3')
214 .having((p0) => p0.$2.color, 'pin', null),
215 ),);
216 expect(records, anyElement(isA<FullEntry>()
217 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1077625200000)
218 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 100)
219 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 82)
220 .having((p0) => p0.$1.pul, 'pulse', 63)
221 .having((p0) => p0.$2.note, 'notes', '')
222 .having((p0) => p0.$2.color, 'pin', null),
223 ),);
224 });
225 test('should import v1.5.7 measurements', () {
226 final text = File('test/model/export_import/exported_formats/v1.5.7.csv').readAsStringSync();
227
228 final converter = CsvConverter(
229 CsvExportSettings(),
230 ExportColumnsManager(),
231 [],
232 );
233 final parsed = converter.parse(text);
234 final records = parsed.getOr(failParse);
235 expect(records, isNotNull);
236 expect(records.length, 185);
237 expect(records, everyElement(isA<FullEntry>()));
238 expect(records, anyElement(isA<FullEntry>()
239 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703175660000)
240 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 312)
241 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 315)
242 .having((p0) => p0.$1.pul, 'pulse', 46)
243 .having((p0) => p0.$2.note, 'notes', 'testfkajkfb')
244 .having((p0) => p0.$2.color, 'pin', null),
245 ),);
246 expect(records, anyElement(isA<FullEntry>()
247 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703175600000)
248 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 123)
249 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 41)
250 .having((p0) => p0.$1.pul, 'pulse', 43)
251 .having((p0) => p0.$2.note, 'notes', '1214s3')
252 .having((p0) => p0.$2.color, 'pin', null),
253 ),);
254 expect(records, anyElement(isA<FullEntry>()
255 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1077625200000)
256 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 100)
257 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 82)
258 .having((p0) => p0.$1.pul, 'pulse', 63)
259 .having((p0) => p0.$2.note, 'notes', '')
260 .having((p0) => p0.$2.color, 'pin', null),
261 ),);
262 // TODO: test color
263 });
264 test('should import v1.5.8 measurements', () {
265 final text = File('test/model/export_import/exported_formats/v1.5.8.csv').readAsStringSync();
266
267 final converter = CsvConverter(
268 CsvExportSettings(),
269 ExportColumnsManager(),
270 [],
271 );
272 final parsed = converter.parse(text);
273 final records = parsed.getOr(failParse);
274 expect(records, isNotNull);
275 expect(records.length, 9478);
276 expect(records, everyElement(isA<FullEntry>()));
277 expect(records, anyElement(isA<FullEntry>()
278 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1703175193324)
279 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 123)
280 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 43)
281 .having((p0) => p0.$1.pul, 'pulse', 53)
282 .having((p0) => p0.$2.note, 'notes', 'sdfsdfds')
283 .having((p0) => p0.$2.color, 'color', 0xff69f0ae),
284 ),);
285 expect(records, anyElement(isA<FullEntry>()
286 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1702883511000)
287 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 114)
288 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 71)
289 .having((p0) => p0.$1.pul, 'pulse', 66)
290 .having((p0) => p0.$2.note, 'notes', 'fsaf &_*¢|^✓[=%®©')
291 .having((p0) => p0.$2.color, 'color', Colors.lightGreen.toARGB32()),
292 ),);
293 expect(records, anyElement(isA<FullEntry>()
294 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1701034952000)
295 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 125)
296 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 77)
297 .having((p0) => p0.$1.pul, 'pulse', 60)
298 .having((p0) => p0.$2.note, 'notes', '')
299 .having((p0) => p0.$2.color, 'color', null),
300 ),);
301 expect(records, anyElement(isA<FullEntry>()
302 .having((p0) => p0.$1.time.millisecondsSinceEpoch, 'timestamp', 1077625200000)
303 .having((p0) => p0.$1.sys?.mmHg, 'systolic', 100)
304 .having((p0) => p0.$1.dia?.mmHg, 'diastolic', 82)
305 .having((p0) => p0.$1.pul, 'pulse', 63)
306 .having((p0) => p0.$2.note, 'notes', '')
307 .having((p0) => p0.$2.color, 'pin', null),
308 ),);
309 });
310 test('should decode formated times', () {
311 final text = File('test/model/export_import/exported_formats/formatted_times.csv').readAsStringSync();
312
313 final cols = ExportColumnsManager();
314 cols.addOrUpdate(TimeColumn('someTime', 'yyyy-MM-dd HH:mm'));
315 final converter = CsvConverter(
316 CsvExportSettings(
317 exportFieldsConfiguration: ActiveExportColumnConfiguration(
318 activePreset: ExportImportPreset.none,
319 userSelectedColumnIds: [],
320 )
321 ),
322 cols,
323 [],
324 );
325 final parsed = converter.parse(text);
326
327 final records = parsed.getOr(failParse);
328 expect(records, isNotNull);
329 expect(records.length, 3);
330 expect(records, contains(isA<FullEntry>()
331 .having((c) => c.sys?.mmHg, 'sys', 1)
332 .having((c) => c.time.year, 'year', 2024)
333 .having((c) => c.time.month, 'month', 3)
334 .having((c) => c.time.day, 'day', 12)
335 .having((c) => c.time.hour, 'hour', 15)
336 .having((c) => c.time.minute, 'minute', 45),
337 ));
338 expect(records, contains(isA<FullEntry>()
339 .having((c) => c.sys?.mmHg, 'sys', 2)
340 .having((c) => c.time.year, 'year', 2004)
341 .having((c) => c.time.month, 'month', 12)
342 .having((c) => c.time.day, 'day', 8)
343 .having((c) => c.time.hour, 'hour', 0)
344 .having((c) => c.time.minute, 'minute', 42),
345 ));
346 expect(records, contains(isA<FullEntry>()
347 .having((c) => c.sys?.mmHg, 'sys', 3)
348 .having((c) => c.time.year, 'year', 2012)
349 .having((c) => c.time.month, 'month', 10)
350 .having((c) => c.time.day, 'day', 8)
351 .having((c) => c.time.hour, 'hour', 0)
352 .having((c) => c.time.minute, 'minute', 4),
353 ));
354 });
355}
356
357List<(DateTime, BloodPressureRecord, Note, List<MedicineIntake>, Null)> createRecords([int count = 20]) => [
358 for (int i = 0; i<count; i++)
359 mockEntryPos(DateTime.fromMillisecondsSinceEpoch(123456 + i),
360 i, 100+i, 200+1, 'note $i', Color(123+i),),
361].map((e) => (e.time, e.recordObj, e.noteObj, e.intakes, null))
362 .toList();
363
364List<FullEntry>? failParse(EntryParsingError error) {
365 switch (error) {
366 case RecordParsingErrorEmptyFile():
367 fail('Parsing failed due to insufficient data.');
368 case RecordParsingErrorTimeNotRestoreable():
369 fail('Parsing failed because time was not parsable.');
370 case RecordParsingErrorUnknownColumn():
371 fail('Parsing failed because column "${error.title}" is unknown.');
372 case RecordParsingErrorExpectedMoreFields():
373 fail('Parsing failed because line ${error.lineNumber} contained not enough fields.');
374 case RecordParsingErrorUnparsableField():
375 fail('Parsing failed because field ${error.fieldContents} in line ${error.lineNumber} is not parsable.');
376 }
377}
378
379// TODO: test csv import actor