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