diff --git a/lib/options.dart b/lib/options.dart index daad5c7287..6724c1423f 100644 --- a/lib/options.dart +++ b/lib/options.dart @@ -127,7 +127,11 @@ DartdocProgramOptionContext? parseOptions( exitCode = 64; return null; } - startLogging(config); + startLogging( + isJson: config.json, + isQuiet: config.quiet, + showProgress: config.showProgress, + ); return config; } diff --git a/lib/src/logging.dart b/lib/src/logging.dart index d11a157bfa..c6c6fb8b69 100644 --- a/lib/src/logging.dart +++ b/lib/src/logging.dart @@ -85,6 +85,9 @@ class _DartdocLogger { static _DartdocLogger instance = _DartdocLogger._(isJson: false, isQuiet: true, showProgress: false); + final StringSink _outSink; + final StringSink _errSink; + final bool _showProgressBar; ProgressBar? _progressBar; @@ -93,7 +96,11 @@ class _DartdocLogger { required bool isJson, required bool isQuiet, required bool showProgress, - }) : _showProgressBar = showProgress && !isJson && !isQuiet { + StringSink? outSink, + StringSink? errSink, + }) : _outSink = outSink ?? io.stdout, + _errSink = errSink ?? io.stderr, + _showProgressBar = showProgress && !isJson && !isQuiet { // By default, get all log output at `progressLevel` or greater. // This allows us to capture progress events and print `...`. // Change this to `Level.FINE` for debug logging. @@ -120,7 +127,7 @@ class _DartdocLogger { Logger.root.onRecord.listen((record) { if (record.level == progressBarUpdate) { - io.stdout.write(record.message); + _outSink.write(record.message); return; } @@ -129,10 +136,10 @@ class _DartdocLogger { showProgress && stopwatch.elapsed.inMilliseconds > 125) { if (writingProgress = false) { - io.stdout.write(' '); + _outSink.write(' '); } writingProgress = true; - io.stdout.write('$_backspace${spinner[spinnerIndex]}'); + _outSink.write('$_backspace${spinner[spinnerIndex]}'); spinnerIndex = (spinnerIndex + 1) % spinner.length; stopwatch.reset(); } @@ -141,22 +148,22 @@ class _DartdocLogger { stopwatch.reset(); if (writingProgress) { - io.stdout.write('$_backspace $_backspace'); + _outSink.write('$_backspace $_backspace'); } var message = record.message; assert(message.isNotEmpty); if (record.level < Level.WARNING) { if (!isQuiet) { - print(message); + _outSink.writeln(message); } } else { if (writingProgress) { // Some console implementations, like IntelliJ, apparently need // the backspace to occur for stderr as well. - io.stderr.write('$_backspace $_backspace'); + _errSink.write('$_backspace $_backspace'); } - io.stderr.writeln(message); + _errSink.writeln(message); } writingProgress = false; }); @@ -204,15 +211,23 @@ class _DartdocLogger { output['message'] = record.message; } - print(json.encode(output)); + _outSink.writeln(json.encode(output)); } } -void startLogging(LoggingContext config) { +void startLogging({ + required bool isJson, + required bool isQuiet, + required bool showProgress, + StringSink? outSink, + StringSink? errSink, +}) { _DartdocLogger.instance = _DartdocLogger._( - isJson: config.json, - isQuiet: config.quiet, - showProgress: config.showProgress, + isJson: isJson, + isQuiet: isQuiet, + showProgress: showProgress, + outSink: outSink ?? io.stdout, + errSink: errSink ?? io.stderr, ); } diff --git a/test/end2end/dartdoc_integration_test.dart b/test/end2end/dartdoc_integration_test.dart index 0ee396f458..cc029ae95f 100644 --- a/test/end2end/dartdoc_integration_test.dart +++ b/test/end2end/dartdoc_integration_test.dart @@ -60,60 +60,6 @@ void main() { ]); }); - test('with --no-generate-docs is quiet and does not generate docs', - () async { - var process = await runDartdoc( - ['--no-generate-docs'], - workingDirectory: packagePath, - ); - await expectLater( - process.stderr, emitsThrough('Found 1 warning and 0 errors.')); - await process.shouldExit(0); - var docs = Directory(path.join(packagePath, 'doc', 'api')); - expect(docs.existsSync(), isFalse); - }); - - test('with --quiet is quiet and does generate docs', () async { - var process = await runDartdoc( - ['--quiet'], - workingDirectory: packagePath, - ); - await expectLater(process.stderr, emitsThrough(matches('^ warning:'))); - await expectLater( - process.stderr, emitsThrough('Found 1 warning and 0 errors.')); - await process.shouldExit(0); - var indexHtml = Directory(path.join(packagePath, 'doc', 'api')); - expect(indexHtml.listSync(), isNotEmpty); - }); - - test('with invalid options return non-zero and print a fatal-error', - () async { - var process = await runDartdoc( - ['--nonexisting'], - workingDirectory: packagePath, - ); - await expectLater( - process.stderr, - emitsThrough( - ' fatal error: Could not find an option named "nonexisting".')); - await process.shouldExit(64); - }); - - test('missing a required file path prints a fatal error', () async { - var process = await runDartdoc( - ['--input', 'non-existant'], - workingDirectory: packagePath, - ); - var fullPath = path.canonicalize(path.join(packagePath, 'non-existant')); - await expectLater( - process.stderr, - emitsThrough( - ' fatal error: Argument --input, set to non-existant, resolves to ' - 'missing path: "$fullPath"'), - ); - await process.shouldExit(64); - }); - test('with --help prints command line args', () async { var process = await runDartdoc( ['--help'], @@ -136,22 +82,6 @@ void main() { emitsThrough('dartdoc version: ${dartdocMeta.version}')); await process.shouldExit(0); }); - - test('Validate JSON output', () async { - var process = await runDartdoc( - [ - //dartdocPath, - '--no-include-source', - '--json', - ], - workingDirectory: packagePath, - ); - await expectLater( - process.stdout, - emitsThrough( - '{"level":"WARNING","message":"Found 1 warning and 0 errors."}')); - await process.shouldExit(0); - }); }); test('with tool errors cause non-zero exit when warnings are off', () async { diff --git a/test/end2end/dartdoc_test.dart b/test/end2end/dartdoc_test.dart index 883067527e..16ed7aec9b 100644 --- a/test/end2end/dartdoc_test.dart +++ b/test/end2end/dartdoc_test.dart @@ -50,10 +50,7 @@ void main() { [createDartdocProgramOptions, createLoggingOptions], pubPackageMetaProvider); optionSet.parseArguments([]); - startLogging(DartdocLoggingOptionContext( - optionSet, - _resourceProvider.getFolder(_pathContext.current), - _resourceProvider)); + startLogging(isJson: false, isQuiet: true, showProgress: false); // Set up the pub metadata for our test packages. runPubGet(testPackageToolError.path); diff --git a/test/options_test.dart b/test/options_test.dart index 915adb5d19..d03d2facdb 100644 --- a/test/options_test.dart +++ b/test/options_test.dart @@ -3,9 +3,12 @@ // BSD-style license that can be found in the LICENSE file. import 'package:analyzer/file_system/memory_file_system.dart'; +import 'package:args/args.dart'; import 'package:dartdoc/options.dart'; import 'package:dartdoc/src/dartdoc.dart'; +import 'package:dartdoc/src/dartdoc_options.dart'; import 'package:dartdoc/src/failure.dart'; +import 'package:dartdoc/src/logging.dart'; import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/package_meta.dart'; import 'package:path/path.dart' as path; @@ -24,15 +27,21 @@ void main() async { late PackageMetaProvider packageMetaProvider; late DartdocGeneratorOptionContext context; + late StringBuffer outBuffer; + late StringBuffer errBuffer; + setUp(() { packageMetaProvider = utils.testPackageMetaProvider; resourceProvider = packageMetaProvider.resourceProvider as MemoryResourceProvider; + outBuffer = StringBuffer(); + errBuffer = StringBuffer(); }); Future createPackageBuilder({ List additionalOptions = const [], bool skipUnreachableSdkLibraries = true, + bool useJson = false, }) async { context = await utils.generatorContextFromArgv([ '--input', @@ -43,6 +52,8 @@ void main() async { packageMetaProvider.defaultSdkDir.path, '--allow-tools', '--no-link-to-remote', + '--no-show-progress', + if (useJson) '--json', ...additionalOptions, ], packageMetaProvider); @@ -50,6 +61,14 @@ void main() async { .getTestPackageConfigProvider(packageMetaProvider.defaultSdkDir.path); packageConfigProvider.addPackageToConfigFor( packagePath, packageName, Uri.file('$packagePath/')); + + startLogging( + isJson: useJson, + isQuiet: true, + showProgress: true, + outSink: outBuffer, + errSink: errBuffer, + ); return PubPackageBuilder( context, packageMetaProvider, @@ -61,10 +80,12 @@ void main() async { Future buildDartdoc({ List additionalOptions = const [], bool skipUnreachableSdkLibraries = true, + bool useJson = false, }) async { final packageBuilder = await createPackageBuilder( additionalOptions: additionalOptions, skipUnreachableSdkLibraries: skipUnreachableSdkLibraries, + useJson: useJson, ); return await Dartdoc.fromContext( context, @@ -591,6 +612,118 @@ class Foo {} 'message', startsWith('Missing required template file')))); }); + test('quiet option results in no progress or other logging', () async { + packagePath = await d.createPackage( + packageName, + libFiles: [ + d.file('library_1.dart', ''' +library library_1; +class Foo {} +'''), + ], + resourceProvider: resourceProvider, + ); + await utils.writeDartdocResources(resourceProvider); + final dartdoc = await buildDartdoc(additionalOptions: [ + '--quiet', + ]); + await dartdoc.generateDocs(); + + // With the `--quiet` option, nothing should be printed to stdout, and only + // warnings should be printed to stderr. + expect(outBuffer, isEmpty); + expect(errBuffer.toString(), matches(RegExp(r''' + warning: library_1 has no library level documentation comments + from library_1: \(.*lib/library_1.dart:1:9\) +Found 1 warning and 0 errors. +'''))); + }); + + test( + 'no-generate-docs option results in no progress or other logging, and no ' + 'generated docs', () async { + packagePath = await d.createPackage( + packageName, + libFiles: [ + d.file('library_1.dart', ''' +library library_1; +class Foo {} +'''), + ], + resourceProvider: resourceProvider, + ); + await utils.writeDartdocResources(resourceProvider); + final dartdoc = await buildDartdoc(additionalOptions: [ + '--no-generate-docs', + ]); + await dartdoc.generateDocs(); + + // With the `--no-generate-docs` option, nothing should be printed to + // stdout, and only warnings should be printed to stderr. + expect(outBuffer, isEmpty); + expect(errBuffer.toString(), matches(RegExp(r''' + warning: library_1 has no library level documentation comments + from library_1: \(.*lib/library_1.dart:1:9\) +Found 1 warning and 0 errors. +'''))); + + final outputDirectory = resourceProvider.getFolder( + path.join(packagePath, 'doc', 'api'), + ); + expect(outputDirectory.exists, isFalse); + }); + + test('json option results in JSON output', () async { + packagePath = await d.createPackage( + packageName, + libFiles: [ + d.file('library_1.dart', ''' +library library_1; +class Foo {} +'''), + ], + resourceProvider: resourceProvider, + ); + await utils.writeDartdocResources(resourceProvider); + final dartdoc = await buildDartdoc(useJson: true); + await dartdoc.generateDocs(); + + expect( + outBuffer.toString().split('\n'), + contains('{"level":"WARNING","message":"Found 1 warning and 0 errors."}'), + ); + expect(errBuffer, isEmpty); + }); + + test('non-existent option results in fatal error', () async { + expect( + () => utils.generatorContextFromArgv([ + '--nonexistent', + ], packageMetaProvider), + throwsA(isA().having( + (e) => e.toString(), + 'toString', + contains('Could not find an option named "nonexistent".'), + )), + ); + }); + + test('non-existent input path results in fatal error', () async { + expect( + () => utils.generatorContextFromArgv([ + '--input', + 'non-existent', + ], packageMetaProvider), + throwsA(isA().having( + (e) => e.message, + 'message', + contains( + 'Argument --input, set to non-existent, resolves to missing path:', + ), + )), + ); + }); + group('limit files created', () { test('maxFileCount is reached', () async { packagePath = await d.createPackage( diff --git a/test/src/utils.dart b/test/src/utils.dart index e61a2c714a..7e6db181cc 100644 --- a/test/src/utils.dart +++ b/test/src/utils.dart @@ -16,6 +16,7 @@ import 'package:dartdoc/src/dartdoc_options.dart'; import 'package:dartdoc/src/failure.dart'; import 'package:dartdoc/src/generator/generator.dart'; import 'package:dartdoc/src/generator/resource_loader.dart'; +import 'package:dartdoc/src/logging.dart'; import 'package:dartdoc/src/markdown_processor.dart'; import 'package:dartdoc/src/matching_link_result.dart'; import 'package:dartdoc/src/model/model_element.dart'; @@ -59,12 +60,15 @@ Future contextFromArgv( Future generatorContextFromArgv( List argv, PackageMetaProvider packageMetaProvider) async { var optionSet = DartdocOptionRoot.fromOptionGenerators( - 'dartdoc', - [ - createDartdocOptions, - createGeneratorOptions, - ], - packageMetaProvider); + 'dartdoc', + [ + createDartdocProgramOptions, + createLoggingOptions, + createDartdocOptions, + createGeneratorOptions, + ], + packageMetaProvider, + ); optionSet.parseArguments(argv); return DartdocGeneratorOptionContext.fromDefaultContextLocation( optionSet, packageMetaProvider.resourceProvider);