api_summary: add basic CLI and validation test

Change-Id: I3e9b5a4c3f1d31967a503c4caf273b9d8f239095
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/502840
Auto-Submit: Kevin Moore <kevmoo@google.com>
Commit-Queue: Kevin Moore <kevmoo@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Paul Berry <paulberry@google.com>
This commit is contained in:
kevmoo
2026-05-12 10:44:46 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 06bfb29baa
commit 58e1cbe1a7
5 changed files with 168 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
package:api_summary/api_summary.dart:
summarizePackage (function: Future<String> Function(String, String, {ApiSummaryCustomizer Function()? createCustomizer}))
ApiSummaryCustomizer (class extends Object, base):
new (constructor: ApiSummaryCustomizer Function())
analysisContext= (setter: AnalysisContext)
packageName= (setter: String)
publicApiLibraries= (setter: Iterable<LibraryElement>)
topLevelPublicElements (getter: Set<Element>)
topLevelPublicElements= (setter: Set<Element>)
initialScanComplete (method: Future<void> Function())
setupComplete (method: Future<void> Function())
shouldShowDetails (method: bool Function(Element))
dart:async:
Future (referenced)
dart:core:
Iterable (referenced)
Object (referenced)
Set (referenced)
String (referenced)
bool (referenced)
package:analyzer/dart/analysis/analysis_context.dart:
AnalysisContext (referenced)
package:analyzer/dart/element/element.dart:
Element (referenced)
LibraryElement (referenced)
+81
View File
@@ -0,0 +1,81 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io';
import 'package:api_summary/api_summary.dart';
import 'package:args/args.dart';
import 'package:path/path.dart' as p;
import 'package:yaml/yaml.dart';
Future<void> main(List<String> arguments) async {
try {
final results = parser.parse(arguments);
if (results.flag('help')) {
print('Usage: api_summary [options]');
print(parser.usage);
return;
}
final packagePath =
results.option('package-path') ?? Directory.current.path;
final absolutePath = p.normalize(p.absolute(packagePath));
final pubspecFile = File(p.join(absolutePath, 'pubspec.yaml'));
if (!pubspecFile.existsSync()) {
stderr.writeln('Error: No pubspec.yaml found at "$absolutePath".');
exitCode = 1;
return;
}
final packageName = _extractPackageName(pubspecFile);
final summary = await summarizePackage(absolutePath, packageName);
stdout.write(summary);
} on FormatException catch (e) {
stderr.writeln('Error: ${e.message}');
stderr.writeln('\nUsage: api_summary [options]');
stderr.writeln(parser.usage);
exitCode = 64;
return;
}
}
final parser = ArgParser()
..addOption(
'package-path',
abbr: 'p',
help:
'The path to the package to summarize. Defaults to the current '
'directory.',
)
..addFlag(
'help',
abbr: 'h',
help: 'Print this usage information.',
negatable: false,
);
String _extractPackageName(File pubspecFile) {
final content = pubspecFile.readAsStringSync();
final yaml = loadYaml(content);
if (yaml is! Map) {
throw ArgumentError(
'Expected pubspec.yaml at ${pubspecFile.path} to be a YAML map.',
);
}
final name = yaml['name'];
if (name == null) {
throw ArgumentError(
'Could not find a "name" field in pubspec.yaml at ${pubspecFile.path}.',
);
}
if (name is! String) {
throw ArgumentError(
'The "name" field in pubspec.yaml at ${pubspecFile.path} must be a '
'String.',
);
}
return name;
}
+3
View File
@@ -11,7 +11,10 @@ resolution: workspace
# Use 'any' constraints here; we get our versions from the DEPS file.
dependencies:
analyzer: any
args: any
collection: any
path: any
yaml: any
# Use 'any' constraints here; we get our versions from the DEPS file.
dev_dependencies:
+58
View File
@@ -0,0 +1,58 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
void main() {
test('api_summary output matches api_summary.txt', () async {
final packageDir = _pkgDir();
final result = await Process.run(Platform.resolvedExecutable, [
if (Platform.packageConfig != null)
'--packages=${Platform.packageConfig}',
p.join(packageDir, 'bin', 'api_summary.dart'),
'-p',
packageDir,
], workingDirectory: packageDir);
expect(
result.exitCode,
equals(0),
reason: 'CLI run failed with stderr:\n${result.stderr}',
);
final goldenFile = File(p.join(packageDir, 'api_summary.txt'));
final expectedOutput = LineSplitter.split(
goldenFile.readAsStringSync(),
).join('\n');
final actualOutput = LineSplitter.split(
result.stdout.toString(),
).join('\n');
expect(actualOutput, equals(expectedOutput));
});
}
// Dynamically locate the api_summary package root
String _pkgDir() {
var packageDir = p.normalize(p.absolute(Directory.current.path));
if (!_isApiSummaryDir(packageDir)) {
// We might be running from the SDK root
final candidate = p.join(packageDir, 'pkg', 'api_summary');
if (_isApiSummaryDir(candidate)) {
packageDir = candidate;
}
}
return packageDir;
}
bool _isApiSummaryDir(String dir) {
final pubspec = File(p.join(dir, 'pubspec.yaml'));
if (!pubspec.existsSync()) return false;
return pubspec.readAsStringSync().contains('name: api_summary');
}
+1
View File
@@ -12,6 +12,7 @@
analyzer/test/verify_diagnostics_test: Slow, Pass
analyzer/test/verify_docs_test: Slow, Pass
analyzer_plugin/test/plugin/folding_mixin_test: Slow, Pass
api_summary/test/app_test: Slow, Pass
compiler/test/analyses/analyze_test: Slow, Pass
compiler/test/analyses/api_dynamic_test: Slow, Pass
compiler/test/closure/closure_test: Slow, Pass