import 'dart:io'; import 'package:pipe/pipe.dart'; import 'package:pipe/src/cli.dart'; import 'package:pipe/src/index.dart'; import 'package:test/test.dart'; /// A directory that is a valid project (it has a package config) but has no Dart /// files to analyse - an empty `lib/`, or a `--dir` that does not exist. String emptyProject(String label) { final temp = Directory.systemTemp.createTempSync('pipe-$label'); final dartTool = Directory('${temp.path}/.dart_tool') ..createSync(recursive: true); File('${dartTool.path}/package_config.json').writeAsStringSync( File( 'test/fixtures/stuck_loading/.dart_tool/package_config.json', ).readAsStringSync(), ); Directory('${temp.path}/lib').createSync(); return temp.path; } void main() { test('a scan that finds no Dart files fails instead of passing quietly', () { // Before this, `pipe check . --dir libs` (a typo) or a project with an empty // lib/ analysed zero files and exited 0, so a CI gate "passed" having // checked nothing at all. Reporting success for an empty scan is the same // class of defect as a test that cannot fail. expect( () => loadProject(emptyProject('empty-lib')), throwsA( isA().having( (error) => error.toString(), 'message', allOf(contains('no Dart files found'), contains('--dir')), ), ), ); }); test( 'a nonexistent --dir fails rather than being skipped silently', () async { final projectPath = emptyProject('missing-dir'); await expectLater( loadProject(projectPath, directories: const ['missing']), throwsA(isA()), ); }, ); test('one mistyped --dir among several is an error, not skipped', () async { // With a single --dir the no-files guard caught a typo, but with several a // missing one was skipped silently and the scan reported on the others as // though it covered what was asked for. await expectLater( loadProject( 'test/fixtures/stuck_loading', directories: const ['lib', 'tset'], ), throwsA( isA().having( (error) => error.toString(), 'message', contains('--dir tset does not exist'), ), ), ); }); test('a real project still loads', () async { final project = await loadProject('test/fixtures/stuck_loading'); expect(project.dartFiles, isNotEmpty); }); group('index text accounting', () { IndexReport report({required int discovered, required int selected}) => IndexReport( root: '/project', sdkPath: null, sdkSource: 'test', isFlutter: false, contextCount: 1, filesDiscovered: discovered, filesSelected: selected, filesResolved: selected, filesSkipped: 0, filesFailed: 0, generatedFiles: 0, diagnosticsBySeverity: const {}, diagnosticsByCode: const {}, unresolvedUris: 0, samples: const [], failures: const [], wall: Duration.zero, perFileMicros: const [], concurrency: 1, ); test('explains the gap when files were filtered out', () { // `--no-include-generated` used to print "13 discovered, 1 generated, // 0 skipped, 0 failed" beside "12/12 resolved", leaving the 13-vs-12 gap // looking like a file had gone missing. final text = report(discovered: 13, selected: 12).describe(); expect(text, contains('13 discovered')); expect(text, contains('12 selected')); expect(text, contains('12/12 resolved')); }); test('stays quiet when nothing was filtered', () { final text = report(discovered: 12, selected: 12).describe(); expect(text, contains('12 discovered')); expect(text, isNot(contains('12 selected'))); }); }); group('limited runs are never silent', () { test( 'produce a warning naming the partial coverage, but only when limited', () { // A truncated analysis used to look exactly like a complete one, and // `verify --limit 2` read the unscanned findings as RESOLVED. expect( limitedRunWarning(limit: null, analysed: 12, discovered: 12), isNull, ); final warning = limitedRunWarning( limit: 2, analysed: 2, discovered: 12, ); expect(warning, isNotNull); expect(warning, contains('--limit 2')); expect(warning, contains('2 of 12')); expect(warning, contains('PARTIAL')); }, ); test('files excluded by analysis_options make a scan partial too', () { // A skipped file was never examined, exactly like a failed one; naming // only failures let a scan with excluded files read as complete. final warning = partialScanWarning(failed: 0, resolved: 11, skipped: 2); expect(warning, isNotNull); expect(warning, contains('2 file(s) were not analysed')); expect(warning, contains('PARTIAL')); }); test('verify refuses a partial scan instead of claiming fixes', () async { final runner = PipeRunner(); await expectLater( runner.run([ 'verify', 'test/fixtures/stuck_loading', '--baseline', '/tmp/sw2/bl1.json', '--limit', '2', '-c', '1', ]), throwsA( isA().having( (error) => error.toString(), 'message', contains('complete scan'), ), ), ); }); }); group('nothing to analyse is never a pass', () { test('every file filtered out by --no-include-generated is an error', () { // loadProject rejects a project with no Dart files at all; this is the // other half - files WERE discovered but the generated filter removed them // all, so the old code analysed nothing and exited 0. final temp = Directory.systemTemp.createTempSync('pipe-onlygen'); final dartTool = Directory('${temp.path}/.dart_tool') ..createSync(recursive: true); File('${dartTool.path}/package_config.json').writeAsStringSync( File( 'test/fixtures/stuck_loading/.dart_tool/package_config.json', ).readAsStringSync(), ); final lib = Directory('${temp.path}/lib')..createSync(); File( '${lib.path}/thing.g.dart', ).writeAsStringSync('class Generated {}\n'); expect( () => resolveProject( Project( root: temp.path, packageConfigPath: '', packageConfig: const {}, flutterRoot: null, dartFiles: ['${lib.path}/thing.g.dart'], ), includeGenerated: false, ), throwsA( isA().having( (error) => error.toString(), 'message', contains('nothing to analyse'), ), ), ); }); }); group('unreadable files are not a clean result', () { test( 'partialScanWarning reports partial coverage, or nothing when clean', () { expect(partialScanWarning(failed: 0, resolved: 12), isNull); final warning = partialScanWarning(failed: 1, resolved: 11); expect(warning, isNotNull); expect(warning, contains('1 file(s) could not be analysed')); expect(warning, contains('11 resolved')); expect(warning, contains('PARTIAL')); }, ); test('files excluded by analysis_options make a scan partial too', () { // A skipped file was never examined, exactly like a failed one; naming // only failures let a scan with excluded files read as complete. final warning = partialScanWarning(failed: 0, resolved: 11, skipped: 2); expect(warning, isNotNull); expect(warning, contains('2 file(s) were not analysed')); expect(warning, contains('PARTIAL')); }); test('a run where every file fails to resolve is an error', () async { // An unreadable file comes back from the analyzer as a unit with EMPTY // content rather than an error, so it used to look like a successful // resolution: a project of 12 unreadable files reported "12/12 resolved" // and `check` said 0 findings, indistinguishable from a clean project. final temp = Directory.systemTemp.createTempSync('pipe-unreadable'); Directory('${temp.path}/lib').createSync(); await expectLater( resolveProject( Project( root: temp.path, packageConfigPath: '', packageConfig: const {}, flutterRoot: null, // A path that cannot be read: the same shape as an unreadable file // or one deleted between discovery and resolution. dartFiles: ['${temp.path}/lib/gone.dart'], ), concurrency: 1, ), throwsA( isA().having( (error) => error.toString(), 'message', contains('no file could be analysed'), ), ), ); }); }); test( 'a run whose every file sits outside any context is an error too', () async { // The guard used to fire only when files FAILED, so a run where every file // was SKIPPED (outside every analysis context) could still report success // having analysed nothing. The test is now simply "nothing was resolved". final temp = Directory.systemTemp.createTempSync('pipe-outside-context'); await expectLater( resolveProject( Project( root: temp.path, packageConfigPath: '', packageConfig: const {}, flutterRoot: null, // Outside the project root, so no analysis context covers it: the // analyzer reports this as skipped, not failed. dartFiles: const ['/etc/hosts'], ), concurrency: 1, ), throwsA( isA().having( (error) => error.toString(), 'message', contains('no file could be analysed'), ), ), ); }, ); }