| // 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 'package:api_summary/api_summary.dart'; |
| import 'package:test/test.dart'; |
| import 'package:test_descriptor/test_descriptor.dart' as d; |
| import 'package:yaml_edit/yaml_edit.dart'; |
| |
| void main() { |
| group('ApiSummary environment and executables', () { |
| test('sorts environment and executables keys alphabetically', () { |
| final summary = ApiSummary( |
| name: 'test_pkg', |
| environment: { |
| 'sdk': '^3.12.0', |
| 'flutter': '>=3.2.0 <3.9.0', |
| 'fuchsia': '>=1.0.0', |
| }, |
| executables: {'z_tool': null, 'a_tool': 'main_a', 'm_tool': 'm_tool'}, |
| libraries: [], |
| ); |
| |
| expect( |
| summary.environment.keys.toList(), |
| equals(['flutter', 'fuchsia', 'sdk']), |
| ); |
| expect( |
| summary.executables.keys.toList(), |
| equals(['a_tool', 'm_tool', 'z_tool']), |
| ); |
| }); |
| |
| test( |
| 'produces identical output regardless of input map insertion order', |
| () { |
| final summaryA = ApiSummary( |
| name: 'test_pkg', |
| environment: {'sdk': '^3.12.0', 'flutter': '>=3.2.0 <3.9.0'}, |
| executables: {'z_tool': null, 'a_tool': 'main_a'}, |
| libraries: [], |
| ); |
| |
| final summaryB = ApiSummary( |
| name: 'test_pkg', |
| environment: {'flutter': '>=3.2.0 <3.9.0', 'sdk': '^3.12.0'}, |
| executables: {'a_tool': 'main_a', 'z_tool': null}, |
| libraries: [], |
| ); |
| |
| expect(summaryA.toString(), equals(summaryB.toString())); |
| expect(summaryA.toJson(), equals(summaryB.toJson())); |
| }, |
| ); |
| |
| test('toJson and fromJson round-trip with environment and executables', () { |
| final summary = ApiSummary( |
| name: 'test_pkg', |
| environment: {'sdk': '^3.12.0', 'flutter': '>=3.2.0 <3.9.0'}, |
| executables: {'cli_a': null, 'cli_b': 'custom_b'}, |
| libraries: [], |
| ); |
| |
| final json = summary.toJson(); |
| expect(json['name'], equals('test_pkg')); |
| expect( |
| json['environment'], |
| equals({'flutter': '>=3.2.0 <3.9.0', 'sdk': '^3.12.0'}), |
| ); |
| expect(json['executables'], equals({'cli_a': null, 'cli_b': 'custom_b'})); |
| |
| final rehydrated = ApiSummary.fromJson(json); |
| expect(rehydrated.name, equals('test_pkg')); |
| expect(rehydrated.environment, equals(summary.environment)); |
| expect(rehydrated.executables, equals(summary.executables)); |
| }); |
| |
| test('toJson omits environment and executables when empty', () { |
| final summary = ApiSummary(name: 'test_pkg', libraries: []); |
| |
| final json = summary.toJson(); |
| expect(json.containsKey('environment'), isFalse); |
| expect(json.containsKey('executables'), isFalse); |
| |
| final rehydrated = ApiSummary.fromJson(json); |
| expect(rehydrated.environment, isEmpty); |
| expect(rehydrated.executables, isEmpty); |
| }); |
| |
| test('text rendering formats environment and executables', () { |
| final summary = ApiSummary( |
| name: 'test_pkg', |
| environment: {'sdk': '^3.12.0', 'flutter': '>=3.2.0 <3.9.0'}, |
| executables: { |
| 'simple_tool': null, |
| 'same_name_tool': 'same_name_tool', |
| 'custom_tool': 'custom_script', |
| }, |
| libraries: [], |
| ); |
| |
| final rendered = summary.toString(); |
| expect( |
| rendered, |
| equals( |
| 'environment:\n' |
| ' flutter: >=3.2.0 <3.9.0\n' |
| ' sdk: ^3.12.0\n' |
| 'executables:\n' |
| ' custom_tool: custom_script\n' |
| ' same_name_tool\n' |
| ' simple_tool\n', |
| ), |
| ); |
| }); |
| |
| test('yaml serialization renders environment and executables', () { |
| final summary = ApiSummary( |
| name: 'test_pkg', |
| environment: {'sdk': '^3.12.0'}, |
| executables: {'my_tool': null}, |
| libraries: [], |
| ); |
| |
| final editor = YamlEditor(''); |
| editor.update([], summary.toJson()); |
| final yaml = '$editor\n'; |
| |
| expect(yaml, contains('environment:\n sdk: ^3.12.0')); |
| expect(yaml, contains('executables:\n my_tool: null')); |
| }); |
| }); |
| |
| group('pubspec.yaml parsing in apiSummary', () { |
| Future<void> withTempPkg( |
| String pubspecContent, |
| Future<void> Function(String pkgPath) fn, |
| ) async { |
| await d.dir('pkg', [ |
| d.file('pubspec.yaml', pubspecContent), |
| d.dir('lib', [d.file('foo.dart', 'void foo() {}')]), |
| ]).create(); |
| |
| await fn(d.path('pkg')); |
| } |
| |
| test('extracts environment and executables successfully', () async { |
| await withTempPkg( |
| ''' |
| name: sample_pkg |
| environment: |
| sdk: ^3.12.0 |
| flutter: ">=3.2.0 <3.9.0" |
| executables: |
| tool_a: |
| tool_b: custom_b |
| ''', |
| (pkgPath) async { |
| final summary = await apiSummary(pkgPath); |
| expect(summary.name, equals('sample_pkg')); |
| expect( |
| summary.environment, |
| equals({'flutter': '>=3.2.0 <3.9.0', 'sdk': '^3.12.0'}), |
| ); |
| expect( |
| summary.executables, |
| equals({'tool_a': null, 'tool_b': 'custom_b'}), |
| ); |
| }, |
| ); |
| }); |
| |
| test('handles omitted and null environment and executables', () async { |
| await withTempPkg( |
| ''' |
| name: minimal_pkg |
| environment: |
| executables: |
| ''', |
| (pkgPath) async { |
| final summary = await apiSummary(pkgPath); |
| expect(summary.name, equals('minimal_pkg')); |
| expect(summary.environment, isEmpty); |
| expect(summary.executables, isEmpty); |
| }, |
| ); |
| }); |
| |
| test('ignores null environment constraints', () async { |
| await withTempPkg( |
| ''' |
| name: sample_pkg |
| environment: |
| sdk: ^3.12.0 |
| flutter: |
| ''', |
| (pkgPath) async { |
| final summary = await apiSummary(pkgPath); |
| expect(summary.environment, equals({'sdk': '^3.12.0'})); |
| }, |
| ); |
| }); |
| |
| test('throws FormatException when pubspec is not a YAML map', () async { |
| await withTempPkg('- item1\n- item2', (pkgPath) async { |
| await expectLater( |
| apiSummary(pkgPath), |
| throwsA( |
| isA<FormatException>().having( |
| (e) => e.message, |
| 'message', |
| contains('Expected pubspec to be a YAML map'), |
| ), |
| ), |
| ); |
| }); |
| }); |
| |
| test( |
| 'throws FormatException when name is missing or not a string', |
| () async { |
| await withTempPkg( |
| ''' |
| environment: |
| sdk: ^3.12.0 |
| ''', |
| (pkgPath) async { |
| await expectLater( |
| apiSummary(pkgPath), |
| throwsA( |
| isA<FormatException>().having( |
| (e) => e.message, |
| 'message', |
| contains('Expected pubspec to contain a "name" string'), |
| ), |
| ), |
| ); |
| }, |
| ); |
| |
| await withTempPkg('name: 12345', (pkgPath) async { |
| await expectLater( |
| apiSummary(pkgPath), |
| throwsA( |
| isA<FormatException>().having( |
| (e) => e.message, |
| 'message', |
| contains('Expected pubspec to contain a "name" string'), |
| ), |
| ), |
| ); |
| }); |
| }, |
| ); |
| |
| test('throws FormatException when environment is not a map', () async { |
| await withTempPkg('name: foo\nenvironment: "sdk ^3.12.0"', ( |
| pkgPath, |
| ) async { |
| await expectLater( |
| apiSummary(pkgPath), |
| throwsA( |
| isA<FormatException>().having( |
| (e) => e.message, |
| 'message', |
| contains('Expected "environment" to be a YAML map'), |
| ), |
| ), |
| ); |
| }); |
| }); |
| |
| test( |
| 'throws FormatException when environment constraint is not a scalar', |
| () async { |
| await withTempPkg( |
| ''' |
| name: foo |
| environment: |
| sdk: [3, 0] |
| ''', |
| (pkgPath) async { |
| await expectLater( |
| apiSummary(pkgPath), |
| throwsA( |
| isA<FormatException>().having( |
| (e) => e.message, |
| 'message', |
| contains( |
| 'Expected environment constraint for "sdk" ' |
| 'to be a string or scalar', |
| ), |
| ), |
| ), |
| ); |
| }, |
| ); |
| |
| await withTempPkg( |
| ''' |
| name: foo |
| environment: |
| sdk: |
| major: 3 |
| ''', |
| (pkgPath) async { |
| await expectLater( |
| apiSummary(pkgPath), |
| throwsA( |
| isA<FormatException>().having( |
| (e) => e.message, |
| 'message', |
| contains( |
| 'Expected environment constraint for "sdk" ' |
| 'to be a string or scalar', |
| ), |
| ), |
| ), |
| ); |
| }, |
| ); |
| }, |
| ); |
| |
| test('throws FormatException when executables is not a map', () async { |
| await withTempPkg('name: foo\nexecutables: "my_tool"', (pkgPath) async { |
| await expectLater( |
| apiSummary(pkgPath), |
| throwsA( |
| isA<FormatException>().having( |
| (e) => e.message, |
| 'message', |
| contains('Expected "executables" to be a YAML map'), |
| ), |
| ), |
| ); |
| }); |
| }); |
| |
| test( |
| 'throws FormatException when executable target is not string or null', |
| () async { |
| await withTempPkg( |
| ''' |
| name: foo |
| executables: |
| my_tool: [invalid, target] |
| ''', |
| (pkgPath) async { |
| await expectLater( |
| apiSummary(pkgPath), |
| throwsA( |
| isA<FormatException>().having( |
| (e) => e.message, |
| 'message', |
| contains( |
| 'Expected executable target for "my_tool" ' |
| 'to be a string or null', |
| ), |
| ), |
| ), |
| ); |
| }, |
| ); |
| }, |
| ); |
| }); |
| } |