2026-04-16 11:48:37 +02:00
|
|
|
#!/usr/bin/env dart
|
|
|
|
|
// Checks that every non-excluded lib/ source file appears in coverage/lcov.info.
|
|
|
|
|
// Run after: flutter test test/unit/ --coverage
|
|
|
|
|
//
|
2026-04-16 13:44:55 +02:00
|
|
|
// To exclude a file add its lib-relative path to [_excluded] below.
|
2026-04-16 11:48:37 +02:00
|
|
|
|
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
2026-04-16 13:44:55 +02:00
|
|
|
// Minimum line-hit percentage across all measured (non-excluded) files.
|
2026-04-20 18:08:09 +02:00
|
|
|
const _minCoveragePercent = 80;
|
2026-04-16 13:44:55 +02:00
|
|
|
|
|
|
|
|
// Pure-abstract interfaces: no executable code, Dart VM never instruments them.
|
|
|
|
|
const _noCode = {
|
2026-05-25 22:18:09 +02:00
|
|
|
'lib/core/db_schema_version.dart',
|
2026-04-16 11:48:37 +02:00
|
|
|
'lib/core/repositories/account_repository.dart',
|
2026-04-18 19:06:02 +02:00
|
|
|
'lib/core/repositories/draft_repository.dart',
|
2026-04-16 11:48:37 +02:00
|
|
|
'lib/core/repositories/email_repository.dart',
|
|
|
|
|
'lib/core/repositories/mailbox_repository.dart',
|
2026-05-16 01:19:01 +02:00
|
|
|
'lib/core/repositories/share_key_repository.dart',
|
2026-04-23 17:43:20 +02:00
|
|
|
'lib/core/repositories/sync_log_repository.dart',
|
2026-05-10 17:41:33 +02:00
|
|
|
'lib/core/repositories/undo_repository.dart',
|
2026-05-14 10:51:28 +02:00
|
|
|
'lib/core/repositories/search_history_repository.dart',
|
2026-05-08 11:14:54 +02:00
|
|
|
'lib/core/models/undo_action.dart',
|
2026-04-17 10:05:31 +02:00
|
|
|
'lib/core/storage/secure_storage.dart',
|
2026-04-16 13:44:55 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Files excluded from the unit-coverage gate because they require integration
|
|
|
|
|
// or widget tests (covered by `task integration` / `task test-flutter`).
|
|
|
|
|
const _excluded = {
|
2026-04-16 11:48:37 +02:00
|
|
|
'lib/data/db/database.dart',
|
|
|
|
|
'lib/data/imap/imap_client_factory.dart',
|
2026-04-29 06:53:31 +02:00
|
|
|
'lib/data/imap/managesieve_client.dart',
|
2026-04-17 10:05:31 +02:00
|
|
|
'lib/data/storage/flutter_secure_storage_impl.dart',
|
2026-04-16 11:48:37 +02:00
|
|
|
'lib/di.dart',
|
|
|
|
|
'lib/main.dart',
|
|
|
|
|
'lib/ui/router.dart',
|
2026-04-23 17:43:20 +02:00
|
|
|
'lib/ui/screens/account_list_screen.dart',
|
2026-05-16 01:19:01 +02:00
|
|
|
'lib/ui/screens/account_receive_screen.dart',
|
|
|
|
|
'lib/ui/screens/account_send_screen.dart',
|
2026-05-11 07:21:15 +02:00
|
|
|
'lib/ui/screens/add_account_screen.dart',
|
2026-04-23 17:43:20 +02:00
|
|
|
'lib/ui/screens/address_emails_screen.dart',
|
2026-05-10 11:27:25 +02:00
|
|
|
'lib/ui/screens/changelog_screen.dart',
|
2026-04-16 11:48:37 +02:00
|
|
|
'lib/ui/screens/compose_screen.dart',
|
2026-05-07 22:07:54 +02:00
|
|
|
'lib/ui/screens/crash_screen.dart',
|
2026-05-11 07:21:15 +02:00
|
|
|
'lib/ui/screens/edit_account_screen.dart',
|
2026-04-16 11:48:37 +02:00
|
|
|
'lib/ui/screens/email_detail_screen.dart',
|
2026-05-08 11:14:54 +02:00
|
|
|
'lib/ui/screens/email_list_screen.dart',
|
2026-04-23 17:43:20 +02:00
|
|
|
'lib/ui/screens/mailbox_list_screen.dart',
|
|
|
|
|
'lib/ui/screens/search_screen.dart',
|
2026-04-25 06:38:21 +02:00
|
|
|
'lib/ui/screens/sieve_script_edit_screen.dart',
|
|
|
|
|
'lib/ui/screens/sieve_scripts_screen.dart',
|
2026-04-21 07:43:30 +02:00
|
|
|
'lib/ui/screens/sync_log_screen.dart',
|
2026-04-25 06:38:21 +02:00
|
|
|
'lib/ui/screens/thread_detail_screen.dart',
|
2026-05-10 10:51:32 +02:00
|
|
|
'lib/ui/screens/undo_log_screen.dart',
|
2026-04-23 17:43:20 +02:00
|
|
|
'lib/ui/widgets/folder_drawer.dart',
|
2026-05-15 08:18:42 +02:00
|
|
|
'lib/ui/widgets/secure_email_webview.dart',
|
2026-05-11 07:21:15 +02:00
|
|
|
'lib/ui/widgets/snooze_picker.dart',
|
|
|
|
|
'lib/ui/widgets/try_connection_button.dart',
|
2026-05-09 18:59:12 +02:00
|
|
|
'lib/ui/widgets/undo_shell.dart',
|
2026-05-17 09:15:53 +02:00
|
|
|
'lib/ui/screens/about_screen.dart',
|
|
|
|
|
'lib/ui/widgets/email_tile.dart',
|
2026-05-11 07:21:15 +02:00
|
|
|
'lib/core/sync/account_sync_manager.dart',
|
2026-05-14 04:06:35 +02:00
|
|
|
'lib/core/sync/background_sync.dart',
|
2026-05-11 07:21:15 +02:00
|
|
|
'lib/core/sync/reliability_runner.dart',
|
2026-04-23 17:43:20 +02:00
|
|
|
'lib/data/jmap/jmap_client.dart',
|
2026-04-25 06:38:21 +02:00
|
|
|
'lib/data/jmap/sieve_repository.dart',
|
2026-04-23 17:43:20 +02:00
|
|
|
'lib/data/repositories/account_repository_impl.dart',
|
2026-05-11 07:21:15 +02:00
|
|
|
'lib/data/repositories/email_repository_impl.dart',
|
|
|
|
|
'lib/data/repositories/mailbox_repository_impl.dart',
|
2026-05-16 01:19:01 +02:00
|
|
|
'lib/data/repositories/share_key_repository_impl.dart',
|
2026-04-23 17:43:20 +02:00
|
|
|
'lib/data/repositories/sync_log_repository_impl.dart',
|
2026-05-10 17:41:33 +02:00
|
|
|
'lib/data/repositories/undo_repository_impl.dart',
|
2026-05-14 10:51:28 +02:00
|
|
|
'lib/data/repositories/search_history_repository_impl.dart',
|
2026-05-14 23:46:29 +02:00
|
|
|
'lib/core/services/update_service.dart',
|
2026-04-16 11:48:37 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void main() {
|
2026-05-07 22:07:54 +02:00
|
|
|
// Check for ghost paths in _excluded and _noCode.
|
|
|
|
|
final allConfiguredPaths = {..._excluded, ..._noCode};
|
|
|
|
|
for (final path in allConfiguredPaths) {
|
|
|
|
|
if (!File(path).existsSync()) {
|
|
|
|
|
stderr.writeln('ERROR: Ghost path found in check_coverage.dart: $path');
|
|
|
|
|
exit(2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 11:48:37 +02:00
|
|
|
final lcovFile = File('coverage/lcov.info');
|
|
|
|
|
final measuredFiles = lcovFile.existsSync()
|
|
|
|
|
? lcovFile
|
|
|
|
|
.readAsLinesSync()
|
|
|
|
|
.where((l) => l.startsWith('SF:'))
|
|
|
|
|
.map((l) => l.substring(3))
|
|
|
|
|
.toSet()
|
|
|
|
|
: <String>{};
|
|
|
|
|
|
|
|
|
|
final sourceFiles = Directory('lib')
|
|
|
|
|
.listSync(recursive: true)
|
|
|
|
|
.whereType<File>()
|
|
|
|
|
.where((f) => f.path.endsWith('.dart') && !f.path.endsWith('.g.dart'))
|
|
|
|
|
.map((f) => f.path.replaceFirst('./', ''))
|
2026-04-16 13:44:55 +02:00
|
|
|
.where((p) => !_excluded.contains(p) && !_noCode.contains(p))
|
2026-04-16 11:48:37 +02:00
|
|
|
.toList()
|
|
|
|
|
..sort();
|
|
|
|
|
|
|
|
|
|
final missing = sourceFiles.where((f) => !measuredFiles.contains(f)).toList();
|
|
|
|
|
|
|
|
|
|
if (missing.isNotEmpty) {
|
|
|
|
|
for (final f in missing) {
|
|
|
|
|
stderr.writeln('MISSING from coverage: $f');
|
|
|
|
|
}
|
|
|
|
|
stderr.writeln(
|
|
|
|
|
'ERROR: ${missing.length} file(s) missing from unit coverage.\n'
|
|
|
|
|
'Add a test or add to _excluded in scripts/check_coverage.dart.',
|
|
|
|
|
);
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 13:44:55 +02:00
|
|
|
// Compute line-hit percentage, skipping excluded files so their 0% lines
|
|
|
|
|
// don't distort the number for genuinely tested code.
|
|
|
|
|
String? currentSf;
|
2026-04-16 11:48:37 +02:00
|
|
|
int total = 0, hits = 0;
|
|
|
|
|
for (final line in lcovFile.readAsLinesSync()) {
|
2026-04-16 13:44:55 +02:00
|
|
|
if (line.startsWith('SF:')) {
|
|
|
|
|
currentSf = line.substring(3);
|
|
|
|
|
} else if (line.startsWith('DA:') &&
|
|
|
|
|
currentSf != null &&
|
2026-04-16 15:14:18 +02:00
|
|
|
!_excluded.contains(currentSf) &&
|
|
|
|
|
!_noCode.contains(currentSf) &&
|
|
|
|
|
!currentSf.endsWith('.g.dart')) {
|
2026-04-16 13:44:55 +02:00
|
|
|
final count = int.parse(line.substring(3).split(',')[1]);
|
|
|
|
|
total++;
|
|
|
|
|
if (count > 0) hits++;
|
|
|
|
|
}
|
2026-04-16 11:48:37 +02:00
|
|
|
}
|
|
|
|
|
final pct = total > 0 ? (hits * 100 ~/ total) : 0;
|
2026-04-16 13:44:55 +02:00
|
|
|
final measuredCount =
|
|
|
|
|
measuredFiles.where((f) => !_excluded.contains(f)).length;
|
2026-04-16 11:48:37 +02:00
|
|
|
stdout.writeln(
|
2026-04-16 13:44:55 +02:00
|
|
|
'coverage: $pct% across $measuredCount measured files'
|
|
|
|
|
' (${_excluded.length} integration-excluded, ${_noCode.length} no-code'
|
|
|
|
|
' — see scripts/check_coverage.dart)',
|
2026-04-16 11:48:37 +02:00
|
|
|
);
|
2026-04-16 13:44:55 +02:00
|
|
|
|
|
|
|
|
if (pct < _minCoveragePercent) {
|
|
|
|
|
stderr.writeln(
|
|
|
|
|
'ERROR: coverage $pct% is below the required $_minCoveragePercent%.',
|
|
|
|
|
);
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
2026-04-16 11:48:37 +02:00
|
|
|
}
|