flutter_flutter/dev/tools/test/create_api_docs_test.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

625 lines
23 KiB
Dart

// Copyright 2014 The Flutter Authors. 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:file/file.dart';
import 'package:file/memory.dart';
import 'package:platform/platform.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:test/test.dart';
import '../../../packages/flutter_tools/test/src/fake_process_manager.dart';
import '../create_api_docs.dart' as apidocs;
import '../dartdoc_checker.dart';
void main() {
group('FlutterInformation', () {
late FakeProcessManager fakeProcessManager;
late FakePlatform fakePlatform;
late MemoryFileSystem memoryFileSystem;
late apidocs.FlutterInformation flutterInformation;
void setUpWithEnvironment(Map<String, String> environment) {
fakePlatform = FakePlatform(environment: environment);
flutterInformation = apidocs.FlutterInformation(
filesystem: memoryFileSystem,
processManager: fakeProcessManager,
platform: fakePlatform,
);
apidocs.FlutterInformation.instance = flutterInformation;
}
setUp(() {
fakeProcessManager = FakeProcessManager.empty();
memoryFileSystem = MemoryFileSystem();
setUpWithEnvironment(<String, String>{});
});
test('getBranchName does not call git if env LUCI_BRANCH provided', () {
setUpWithEnvironment(<String, String>{'LUCI_BRANCH': branchName});
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
);
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
);
expect(apidocs.FlutterInformation.instance.getBranchName(), branchName);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
test('getBranchName calls git if env LUCI_BRANCH not provided', () {
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
);
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
);
expect(apidocs.FlutterInformation.instance.getBranchName(), branchName);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
test('getBranchName calls git if env LUCI_BRANCH is empty', () {
setUpWithEnvironment(<String, String>{'LUCI_BRANCH': ''});
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
);
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
);
expect(apidocs.FlutterInformation.instance.getBranchName(), branchName);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
test("runPubProcess doesn't use the pub binary", () {
final Platform platform = FakePlatform(
environment: <String, String>{'FLUTTER_ROOT': '/flutter'},
);
final ProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', '--one', '--two']),
]);
apidocs.FlutterInformation.instance = apidocs.FlutterInformation(
platform: platform,
processManager: processManager,
filesystem: memoryFileSystem,
);
apidocs.runPubProcess(
arguments: <String>['--one', '--two'],
processManager: processManager,
filesystem: memoryFileSystem,
);
expect(processManager, hasNoRemainingExpectations);
});
test('calls out to flutter if FLUTTER_VERSION is not set', () async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
);
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
);
final Map<String, dynamic> info = flutterInformation.getFlutterInformation();
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(info['frameworkVersion'], equals(Version.parse('2.5.0')));
});
test("doesn't call out to flutter if FLUTTER_VERSION is set", () async {
setUpWithEnvironment(<String, String>{'FLUTTER_VERSION': testVersionInfo});
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
);
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
);
final Map<String, dynamic> info = flutterInformation.getFlutterInformation();
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(info['frameworkVersion'], equals(Version.parse('2.5.0')));
});
test('getFlutterRoot calls out to flutter if FLUTTER_ROOT is not set', () async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
);
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
);
final Directory root = flutterInformation.getFlutterRoot();
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(root.path, equals('/home/user/flutter'));
});
test("getFlutterRoot doesn't call out to flutter if FLUTTER_ROOT is set", () async {
setUpWithEnvironment(<String, String>{'FLUTTER_ROOT': '/home/user/flutter'});
final Directory root = flutterInformation.getFlutterRoot();
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(root.path, equals('/home/user/flutter'));
});
test('parses version properly', () async {
fakePlatform.environment['FLUTTER_VERSION'] = testVersionInfo;
fakeProcessManager.addCommands(<FakeCommand>[
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
]);
final Map<String, dynamic> info = flutterInformation.getFlutterInformation();
expect(info['frameworkVersion'], isNotNull);
expect(info['frameworkVersion'], equals(Version.parse('2.5.0')));
expect(info['dartSdkVersion'], isNotNull);
expect(info['dartSdkVersion'], equals(Version.parse('2.14.0-360.0.dev')));
});
test('the engine realm is read from the engine.realm file', () async {
final Directory flutterHome = memoryFileSystem
.directory('/home')
.childDirectory('user')
.childDirectory('flutter')
.childDirectory('bin')
.childDirectory('cache');
flutterHome.childFile('engine.realm')
..createSync(recursive: true)
..writeAsStringSync('realm');
setUpWithEnvironment(<String, String>{'FLUTTER_ROOT': '/home/user/flutter'});
fakeProcessManager.addCommands(<FakeCommand>[
const FakeCommand(
command: <Pattern>['/home/user/flutter/bin/flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
]);
final Map<String, dynamic> info = flutterInformation.getFlutterInformation();
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(info['engineRealm'], equals('realm'));
});
});
group('Configurator', () {
late MemoryFileSystem fs;
late FakeProcessManager fakeProcessManager;
late Directory publishRoot;
late Directory packageRoot;
late Directory docsRoot;
late File searchTemplate;
late apidocs.Configurator configurator;
late FakePlatform fakePlatform;
late apidocs.FlutterInformation flutterInformation;
void setUpWithEnvironment(Map<String, String> environment) {
fakePlatform = FakePlatform(environment: environment);
flutterInformation = apidocs.FlutterInformation(
filesystem: fs,
processManager: fakeProcessManager,
platform: fakePlatform,
);
apidocs.FlutterInformation.instance = flutterInformation;
}
setUp(() {
fs = MemoryFileSystem.test();
publishRoot = fs.directory('/path/to/publish');
packageRoot = fs.directory('/path/to/package');
docsRoot = fs.directory('/path/to/docs');
searchTemplate = docsRoot.childDirectory('lib').childFile('opensearch.xml');
fs.directory('/home/user/flutter/packages').createSync(recursive: true);
fakeProcessManager = FakeProcessManager.empty();
setUpWithEnvironment(<String, String>{});
publishRoot.createSync(recursive: true);
packageRoot.createSync(recursive: true);
docsRoot.createSync(recursive: true);
final files = <String>[
'README.md',
'analysis_options.yaml',
'dartdoc_options.yaml',
searchTemplate.path,
publishRoot.childFile('opensearch.xml').path,
];
for (final file in files) {
docsRoot.childFile(file).createSync(recursive: true);
}
searchTemplate.writeAsStringSync('{SITE_URL}');
configurator = apidocs.Configurator(
docsRoot: docsRoot,
packageRoot: packageRoot,
publishRoot: publishRoot,
filesystem: fs,
processManager: fakeProcessManager,
platform: fakePlatform,
);
fakeProcessManager.addCommands(<FakeCommand>[
const FakeCommand(
command: <String>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list']),
FakeCommand(
command: <Pattern>[
'/flutter/bin/flutter',
'pub',
'global',
'run',
'--enable-asserts',
'dartdoc',
'--output',
'/path/to/publish/flutter',
'--allow-tools',
'--json',
'--validate-links',
'--link-to-source-excludes',
'/flutter/bin/cache',
'--link-to-source-root',
'/flutter',
'--link-to-source-uri-template',
'https://github.com/flutter/flutter/blob/main/%f%#L%l%',
'--inject-html',
'--use-base-href',
'--header',
'/path/to/docs/styles.html',
'--header',
'/path/to/docs/analytics-header.html',
'--header',
'/path/to/docs/survey.html',
'--header',
'/path/to/docs/snippets.html',
'--header',
'/path/to/docs/opensearch.html',
'--footer',
'/path/to/docs/analytics-footer.html',
'--footer-text',
'/path/to/package/footer.html',
'--allow-warnings-in-packages',
// match package names
RegExp(r'^(\w+,)+(\w+)$'),
'--exclude-packages',
RegExp(r'^(\w+,)+(\w+)$'),
'--exclude',
// match dart package URIs
RegExp(r'^([\w\/:.]+,)+([\w\/:.]+)$'),
'--favicon',
'/path/to/docs/favicon.ico',
'--package-order',
'flutter,Dart,${apidocs.kPlatformIntegrationPackageName},flutter_test,flutter_driver',
'--auto-include-dependencies',
],
),
]);
});
test('.generateConfiguration generates pubspec.yaml', () async {
configurator.generateConfiguration();
expect(packageRoot.childFile('pubspec.yaml').existsSync(), isTrue);
final String pubspecContents = packageRoot.childFile('pubspec.yaml').readAsStringSync();
expect(pubspecContents, contains('flutter_gpu:'));
expect(pubspecContents, contains("sdk: '^2.14.0-360.0.dev'"));
expect(pubspecContents, contains('dependency_overrides:'));
expect(pubspecContents, contains('platform_integration:'));
});
test('.generateConfiguration generates fake lib', () async {
configurator.generateConfiguration();
expect(packageRoot.childDirectory('lib').existsSync(), isTrue);
expect(packageRoot.childDirectory('lib').childFile('temp_doc.dart').existsSync(), isTrue);
expect(
packageRoot.childDirectory('lib').childFile('temp_doc.dart').readAsStringSync(),
contains('library temp_doc;'),
);
expect(
packageRoot.childDirectory('lib').childFile('temp_doc.dart').readAsStringSync(),
contains("import 'package:flutter_gpu/gpu.dart';"),
);
});
test('.generateConfiguration generates page footer', () async {
configurator.generateConfiguration();
expect(packageRoot.childFile('footer.html').existsSync(), isTrue);
expect(
packageRoot.childFile('footer.html').readAsStringSync(),
contains('<script src="footer.js">'),
);
expect(publishRoot.childDirectory('flutter').childFile('footer.js').existsSync(), isTrue);
expect(
publishRoot.childDirectory('flutter').childFile('footer.js').readAsStringSync(),
contains(RegExp(r'Flutter 2.5.0 •.*• stable')),
);
});
test('.generateConfiguration generates search metadata', () async {
configurator.generateConfiguration();
expect(publishRoot.childFile('opensearch.xml').existsSync(), isTrue);
expect(
publishRoot.childFile('opensearch.xml').readAsStringSync(),
contains('https://api.flutter.dev/'),
);
});
});
group('DartDocGenerator', () {
late apidocs.DartdocGenerator generator;
late MemoryFileSystem fs;
late FakeProcessManager processManager;
late Directory publishRoot;
setUp(() {
fs = MemoryFileSystem.test();
publishRoot = fs.directory('/path/to/publish');
processManager = FakeProcessManager.empty();
generator = apidocs.DartdocGenerator(
packageRoot: fs.directory('/path/to/package'),
publishRoot: publishRoot,
docsRoot: fs.directory('/path/to/docs'),
filesystem: fs,
processManager: processManager,
);
final Directory repoRoot = fs.directory('/flutter');
repoRoot.childDirectory('packages').createSync(recursive: true);
apidocs.FlutterInformation.instance = apidocs.FlutterInformation(
filesystem: fs,
processManager: processManager,
platform: FakePlatform(environment: <String, String>{'FLUTTER_ROOT': repoRoot.path}),
);
});
test('.generateDartDoc() invokes dartdoc with the correct command line arguments', () async {
processManager.addCommands(<FakeCommand>[
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', 'get']),
const FakeCommand(
command: <String>['/flutter/bin/flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list']),
FakeCommand(
command: <Pattern>[
'/flutter/bin/flutter',
'pub',
'global',
'run',
'--enable-asserts',
'dartdoc',
'--output',
'/path/to/publish/flutter',
'--allow-tools',
'--json',
'--validate-links',
'--link-to-source-excludes',
'/flutter/bin/cache',
'--link-to-source-root',
'/flutter',
'--link-to-source-uri-template',
'https://github.com/flutter/flutter/blob/main/%f%#L%l%',
'--inject-html',
'--use-base-href',
'--header',
'/path/to/docs/styles.html',
'--header',
'/path/to/docs/analytics-header.html',
'--header',
'/path/to/docs/survey.html',
'--header',
'/path/to/docs/snippets.html',
'--header',
'/path/to/docs/opensearch.html',
'--footer',
'/path/to/docs/analytics-footer.html',
'--footer-text',
'/path/to/package/footer.html',
'--allow-warnings-in-packages',
// match package names
RegExp(r'^(\w+,)+(\w+)$'),
'--exclude-packages',
RegExp(r'^(\w+,)+(\w+)$'),
'--exclude',
// match dart package URIs
RegExp(r'^([\w\/:.]+,)+([\w\/:.]+)$'),
'--favicon',
'/path/to/docs/favicon.ico',
'--package-order',
'flutter,Dart,${apidocs.kPlatformIntegrationPackageName},flutter_test,flutter_driver',
'--auto-include-dependencies',
],
),
]);
// This will throw while sanity checking generated files, which is tested independently
await expectLater(
() => generator.generateDartdoc(),
throwsA(
isA<Exception>().having(
(Exception e) => e.toString(),
'message',
contains(
RegExp(
r'Missing .* which probably means the documentation failed to build correctly.',
),
),
),
),
);
expect(processManager, hasNoRemainingExpectations);
});
test('sanity checks spot check generated files', () async {
processManager.addCommands(<FakeCommand>[
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', 'get']),
const FakeCommand(
command: <String>['/flutter/bin/flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list']),
FakeCommand(
command: <Pattern>[
'/flutter/bin/flutter',
'pub',
'global',
'run',
'--enable-asserts',
'dartdoc',
'--output',
'/path/to/publish/flutter',
'--allow-tools',
'--json',
'--validate-links',
'--link-to-source-excludes',
'/flutter/bin/cache',
'--link-to-source-root',
'/flutter',
'--link-to-source-uri-template',
'https://github.com/flutter/flutter/blob/main/%f%#L%l%',
'--inject-html',
'--use-base-href',
'--header',
'/path/to/docs/styles.html',
'--header',
'/path/to/docs/analytics-header.html',
'--header',
'/path/to/docs/survey.html',
'--header',
'/path/to/docs/snippets.html',
'--header',
'/path/to/docs/opensearch.html',
'--footer',
'/path/to/docs/analytics-footer.html',
'--footer-text',
'/path/to/package/footer.html',
'--allow-warnings-in-packages',
// match package names
RegExp(r'^(\w+,)+(\w+)$'),
'--exclude-packages',
RegExp(r'^(\w+,)+(\w+)$'),
'--exclude',
// match dart package URIs
RegExp(r'^([\w\/:.]+,)+([\w\/:.]+)$'),
'--favicon',
'/path/to/docs/favicon.ico',
'--package-order',
'flutter,Dart,${apidocs.kPlatformIntegrationPackageName},flutter_test,flutter_driver',
'--auto-include-dependencies',
],
onRun: (_) {
for (final File canary in generator.canaries) {
canary.createSync(recursive: true);
}
for (final String path in dartdocDirectiveCanaryFiles) {
publishRoot.childDirectory('flutter').childFile(path).createSync(recursive: true);
}
for (final String path in dartdocDirectiveCanaryLibraries) {
publishRoot
.childDirectory('flutter')
.childDirectory(path)
.createSync(recursive: true);
}
publishRoot.childDirectory('flutter').childFile('index.html').createSync();
final Directory widgetsDir =
publishRoot.childDirectory('flutter').childDirectory('widgets')
..createSync(recursive: true);
widgetsDir.childFile('showGeneralDialog.html').writeAsStringSync('''
<pre id="longSnippet1">
<code class="language-dart">
import &#39;package:flutter&#47;material.dart&#39;;
</code>
</pre>
''');
expect(publishRoot.childDirectory('flutter').existsSync(), isTrue);
(widgetsDir.childDirectory(
'ModalRoute',
)..createSync(recursive: true)).childFile('barrierColor.html').writeAsStringSync('''
<pre id="sample-code">
<code class="language-dart">
class FooClass {
Color get barrierColor => FooColor();
}
</code>
</pre>
''');
const queryParams = 'split=1&run=true&sample_id=widgets.Listener.123&channel=main';
widgetsDir.childFile('Listener-class.html').writeAsStringSync('''
<iframe class="snippet-dartpad" src="https://dartpad.dev/embed-flutter.html?$queryParams">
</iframe>
''');
},
),
]);
await generator.generateDartdoc();
});
});
}
const String branchName = 'stable';
const String testVersionInfo =
'''
{
"frameworkVersion": "2.5.0",
"channel": "$branchName",
"repositoryUrl": "git@github.com:flutter/flutter.git",
"frameworkRevision": "0000000000000000000000000000000000000000",
"frameworkCommitDate": "2021-07-28 13:03:40 -0700",
"engineRevision": "0000000000000000000000000000000000000001",
"dartSdkVersion": "2.14.0 (build 2.14.0-360.0.dev)",
"flutterRoot": "/home/user/flutter"
}
''';