Unverified Commit 8b768de1 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add lint check to make sure samples are linked and have tests (#130523)

## Description

This adds a checker that will check all of the API docs examples to make sure that they are linked from at least one source file in the packages.

It also checks to make sure that all of the examples have an associated test. Since there are a large number that don't have tests at the moment, there is also a large exception list that can be burned down (burn down list is in https://github.com/flutter/flutter/issues/130459).

Because there are missing links currently, this PR will only pass after https://github.com/flutter/flutter/pull/130521 is merged.

## Related Issues

- https://github.com/flutter/flutter/issues/129956
- https://github.com/flutter/flutter/issues/130459

## Tests

- Added test for the checker.
parent 3b0b8348
......@@ -187,6 +187,13 @@ Future<void> run(List<String> arguments) async {
workingDirectory: flutterRoot,
);
// Make sure that all of the existing samples are linked from at least one API doc comment.
printProgress('Code sample link validation...');
await runCommand(dart,
<String>['--enable-asserts', path.join(flutterRoot, 'dev', 'bots', 'check_code_samples.dart')],
workingDirectory: flutterRoot,
);
// Try analysis against a big version of the gallery; generate into a temporary directory.
printProgress('Dart analysis (mega gallery)...');
final Directory outDir = Directory.systemTemp.createTempSync('flutter_mega_gallery.');
......
......@@ -5,7 +5,7 @@
// To run this, from the root of the Flutter repository:
// bin/cache/dart-sdk/bin/dart --enable-asserts dev/bots/analyze_snippet_code.dart
// In general, please prefer using full inline examples in API docs.
// In general, please prefer using full linked examples in API docs.
//
// For documentation on creating sample code, see ../../examples/api/README.md
// See also our style guide's discussion on documentation and sample code:
......@@ -13,7 +13,7 @@
//
// This tool is used to analyze smaller snippets of code in the API docs.
// Such snippets are wrapped in ```dart ... ``` blocks, which may themselves
// be wrapped in {@tool snippet} ... {@endtool} blocks to set them apart
// be wrapped in {@tool snippet} ... {@end-tool} blocks to set them apart
// in the rendered output.
//
// Such snippets:
......
// 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.
// To run this, from the root of the Flutter repository:
// bin/cache/dart-sdk/bin/dart --enable-asserts dev/bots/check_code_sample_links.dart
import 'dart:io';
import 'package:args/args.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:path/path.dart' as path;
import 'utils.dart';
final String _scriptLocation = path.fromUri(Platform.script);
final String _flutterRoot = path.dirname(path.dirname(path.dirname(_scriptLocation)));
final String _exampleDirectoryPath = path.join(_flutterRoot, 'examples', 'api');
final String _packageDirectoryPath = path.join(_flutterRoot, 'packages');
final String _dartUIDirectoryPath = path.join(_flutterRoot, 'bin', 'cache', 'pkg', 'sky_engine', 'lib');
final List<String> _knownUnlinkedExamples = <String>[
// These are template files that aren't expected to be linked.
'examples/api/lib/sample_templates/cupertino.0.dart',
'examples/api/lib/sample_templates/widgets.0.dart',
'examples/api/lib/sample_templates/material.0.dart',
];
void main(List<String> args) {
final ArgParser argParser = ArgParser();
argParser.addFlag(
'help',
negatable: false,
help: 'Print help for this command.',
);
argParser.addOption(
'examples',
valueHelp: 'path',
defaultsTo: _exampleDirectoryPath,
help: 'A location where the API doc examples are found.',
);
argParser.addOption(
'packages',
valueHelp: 'path',
defaultsTo: _packageDirectoryPath,
help: 'A location where the source code that should link the API doc examples is found.',
);
argParser.addOption(
'dart-ui',
valueHelp: 'path',
defaultsTo: _dartUIDirectoryPath,
help: 'A location where the source code that should link the API doc examples is found.',
);
argParser.addOption(
'flutter-root',
valueHelp: 'path',
defaultsTo: _flutterRoot,
help: 'The path to the root of the Flutter repo.',
);
final ArgResults parsedArgs;
void usage() {
print('dart --enable-asserts ${path.basename(_scriptLocation)} [options]');
print(argParser.usage);
}
try {
parsedArgs = argParser.parse(args);
} on FormatException catch (e) {
print(e.message);
usage();
exit(1);
}
if (parsedArgs['help'] as bool) {
usage();
exit(0);
}
const FileSystem filesystem = LocalFileSystem();
final Directory examples = filesystem.directory(parsedArgs['examples']! as String);
final Directory packages = filesystem.directory(parsedArgs['packages']! as String);
final Directory dartUIPath = filesystem.directory(parsedArgs['dart-ui']! as String);
final Directory flutterRoot = filesystem.directory(parsedArgs['flutter-root']! as String);
final SampleChecker checker = SampleChecker(
examples: examples,
packages: packages,
dartUIPath: dartUIPath,
flutterRoot: flutterRoot,
);
if (!checker.checkCodeSamples()) {
reportErrorsAndExit('Some errors were found in the API docs code samples.');
}
reportSuccessAndExit('All examples are linked and have tests.');
}
class SampleChecker {
SampleChecker({
required this.examples,
required this.packages,
required this.dartUIPath,
required this.flutterRoot,
this.filesystem = const LocalFileSystem(),
});
final Directory examples;
final Directory packages;
final Directory dartUIPath;
final Directory flutterRoot;
final FileSystem filesystem;
bool checkCodeSamples() {
filesystem.currentDirectory = flutterRoot;
// Get a list of all the filenames in the source directory that end in "[0-9]+.dart".
final List<File> exampleFilenames = getExampleFilenames(examples);
// Get a list of all the example link paths that appear in the source files.
final Set<String> exampleLinks = getExampleLinks(packages);
// Also add in any that might be found in the dart:ui directory.
exampleLinks.addAll(getExampleLinks(dartUIPath));
// Get a list of the filenames that were not found in the source files.
final List<String> missingFilenames = checkForMissingLinks(exampleFilenames, exampleLinks);
// Get a list of any tests that are missing, as well as any that used to be
// missing, but have been implemented.
final (List<File> missingTests, List<File> noLongerMissing) = checkForMissingTests(exampleFilenames);
// Remove any that we know are exceptions (examples that aren't expected to be
// linked into any source files). These are typically template files used to
// generate new examples.
missingFilenames.removeWhere((String file) => _knownUnlinkedExamples.contains(file));
if (missingFilenames.isEmpty && missingTests.isEmpty && noLongerMissing.isEmpty) {
return true;
}
if (noLongerMissing.isNotEmpty) {
final StringBuffer buffer = StringBuffer('The following tests have been implemented! Huzzah!:\n');
for (final File name in noLongerMissing) {
buffer.writeln(' ${getRelativePath(name)}');
}
buffer.writeln('However, they now need to be removed from the _knownMissingTests');
buffer.write('list in the script $_scriptLocation.');
foundError(buffer.toString().split('\n'));
}
if (missingTests.isNotEmpty) {
final StringBuffer buffer = StringBuffer('The following example test files are missing:\n');
for (final File name in missingTests) {
buffer.writeln(' ${getRelativePath(name)}');
}
foundError(buffer.toString().trimRight().split('\n'));
}
if (missingFilenames.isNotEmpty) {
final StringBuffer buffer =
StringBuffer('The following examples are not linked from any source file API doc comments:\n');
for (final String name in missingFilenames) {
buffer.writeln(' $name');
}
buffer.write('Either link them to a source file API doc comment, or remove them.');
foundError(buffer.toString().split('\n'));
}
return false;
}
String getRelativePath(File file, [Directory? root]) {
root ??= flutterRoot;
return path.relative(file.absolute.path, from: root.absolute.path);
}
List<File> getFiles(Directory directory, [Pattern? filenamePattern]) {
final List<File> filenames = directory
.listSync(recursive: true)
.map((FileSystemEntity entity) {
if (entity is File) {
return entity;
} else {
return null;
}
})
.where((File? filename) =>
filename != null && (filenamePattern == null || filename.absolute.path.contains(filenamePattern)))
.map<File>((File? s) => s!)
.toList();
return filenames;
}
List<File> getExampleFilenames(Directory directory) {
return getFiles(
directory.childDirectory('lib'),
RegExp(r'\d+\.dart$'),
);
}
Set<String> getExampleLinks(Directory searchDirectory) {
final List<File> files = getFiles(searchDirectory, RegExp(r'\.dart$'));
final Set<String> searchStrings = <String>{};
final RegExp exampleRe = RegExp(r'\*\* See code in (?<path>.*) \*\*');
for (final File file in files) {
final String contents = file.readAsStringSync();
searchStrings.addAll(
contents.split('\n').where((String s) => s.contains(exampleRe)).map<String>(
(String e) {
return exampleRe.firstMatch(e)!.namedGroup('path')!;
},
),
);
}
return searchStrings;
}
List<String> checkForMissingLinks(List<File> exampleFilenames, Set<String> searchStrings) {
final List<String> missingFilenames = <String>[];
for (final File example in exampleFilenames) {
final String relativePath = getRelativePath(example);
if (!searchStrings.contains(relativePath)) {
missingFilenames.add(relativePath);
}
}
return missingFilenames;
}
String getTestNameForExample(File example, Directory examples) {
final String testPath = path.dirname(
path.join(
examples.absolute.path,
'test',
getRelativePath(example, examples.childDirectory('lib')),
),
);
return '${path.join(testPath, path.basenameWithoutExtension(example.path))}_test.dart';
}
(List<File>, List<File>) checkForMissingTests(List<File> exampleFilenames) {
final List<File> missingTests = <File>[];
final List<File> noLongerMissingTests = <File>[];
for (final File example in exampleFilenames) {
final File testFile = filesystem.file(getTestNameForExample(example, examples));
final String name = path.relative(testFile.absolute.path, from: flutterRoot.absolute.path);
if (!testFile.existsSync()) {
missingTests.add(testFile);
} else if (_knownMissingTests.contains(name.replaceAll(r'\', '/'))) {
noLongerMissingTests.add(testFile);
}
}
// Skip any that we know are missing.
missingTests.removeWhere(
(File test) {
final String name = path.relative(test.absolute.path, from: flutterRoot.absolute.path).replaceAll(r'\', '/');
return _knownMissingTests.contains(name);
},
);
return (missingTests, noLongerMissingTests);
}
}
// These tests are known to be missing. They should all eventually be
// implemented, but until they are we allow them, so that we can catch any new
// examples that are added without tests.
//
// TODO(gspencergoog): implement the missing tests.
// See https://github.com/flutter/flutter/issues/130459
final Set<String> _knownMissingTests = <String>{
'examples/api/test/cupertino/text_field/cupertino_text_field.0_test.dart',
'examples/api/test/material/bottom_app_bar/bottom_app_bar.2_test.dart',
'examples/api/test/material/bottom_app_bar/bottom_app_bar.1_test.dart',
'examples/api/test/material/theme/theme_extension.1_test.dart',
'examples/api/test/material/elevated_button/elevated_button.0_test.dart',
'examples/api/test/material/material_state/material_state_border_side.0_test.dart',
'examples/api/test/material/material_state/material_state_mouse_cursor.0_test.dart',
'examples/api/test/material/material_state/material_state_outlined_border.0_test.dart',
'examples/api/test/material/material_state/material_state_property.0_test.dart',
'examples/api/test/material/selectable_region/selectable_region.0_test.dart',
'examples/api/test/material/text_field/text_field.2_test.dart',
'examples/api/test/material/text_field/text_field.1_test.dart',
'examples/api/test/material/button_style/button_style.0_test.dart',
'examples/api/test/material/range_slider/range_slider.0_test.dart',
'examples/api/test/material/card/card.2_test.dart',
'examples/api/test/material/card/card.0_test.dart',
'examples/api/test/material/selection_container/selection_container_disabled.0_test.dart',
'examples/api/test/material/selection_container/selection_container.0_test.dart',
'examples/api/test/material/color_scheme/dynamic_content_color.0_test.dart',
'examples/api/test/material/platform_menu_bar/platform_menu_bar.0_test.dart',
'examples/api/test/material/menu_anchor/menu_anchor.2_test.dart',
'examples/api/test/material/stepper/stepper.controls_builder.0_test.dart',
'examples/api/test/material/stepper/stepper.0_test.dart',
'examples/api/test/material/flexible_space_bar/flexible_space_bar.0_test.dart',
'examples/api/test/material/data_table/data_table.1_test.dart',
'examples/api/test/material/data_table/data_table.0_test.dart',
'examples/api/test/material/floating_action_button_location/standard_fab_location.0_test.dart',
'examples/api/test/material/chip/deletable_chip_attributes.on_deleted.0_test.dart',
'examples/api/test/material/snack_bar/snack_bar.0_test.dart',
'examples/api/test/material/snack_bar/snack_bar.2_test.dart',
'examples/api/test/material/snack_bar/snack_bar.1_test.dart',
'examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.0_test.dart',
'examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.1_test.dart',
'examples/api/test/material/outlined_button/outlined_button.0_test.dart',
'examples/api/test/material/icon_button/icon_button.2_test.dart',
'examples/api/test/material/icon_button/icon_button.3_test.dart',
'examples/api/test/material/icon_button/icon_button.0_test.dart',
'examples/api/test/material/icon_button/icon_button.1_test.dart',
'examples/api/test/material/expansion_panel/expansion_panel_list.0_test.dart',
'examples/api/test/material/expansion_panel/expansion_panel_list.expansion_panel_list_radio.0_test.dart',
'examples/api/test/material/input_decorator/input_decoration.1_test.dart',
'examples/api/test/material/input_decorator/input_decoration.prefix_icon_constraints.0_test.dart',
'examples/api/test/material/input_decorator/input_decoration.material_state.0_test.dart',
'examples/api/test/material/input_decorator/input_decoration.2_test.dart',
'examples/api/test/material/input_decorator/input_decoration.0_test.dart',
'examples/api/test/material/input_decorator/input_decoration.label.0_test.dart',
'examples/api/test/material/input_decorator/input_decoration.suffix_icon_constraints.0_test.dart',
'examples/api/test/material/input_decorator/input_decoration.3_test.dart',
'examples/api/test/material/input_decorator/input_decoration.material_state.1_test.dart',
'examples/api/test/material/filled_button/filled_button.0_test.dart',
'examples/api/test/material/text_form_field/text_form_field.1_test.dart',
'examples/api/test/material/scrollbar/scrollbar.1_test.dart',
'examples/api/test/material/scrollbar/scrollbar.0_test.dart',
'examples/api/test/material/dropdown_menu/dropdown_menu.1_test.dart',
'examples/api/test/material/dropdown_menu/dropdown_menu.0_test.dart',
'examples/api/test/material/radio/radio.toggleable.0_test.dart',
'examples/api/test/material/radio/radio.0_test.dart',
'examples/api/test/material/search_anchor/search_anchor.0_test.dart',
'examples/api/test/material/search_anchor/search_anchor.1_test.dart',
'examples/api/test/material/search_anchor/search_anchor.2_test.dart',
'examples/api/test/material/about/about_list_tile.0_test.dart',
'examples/api/test/material/tab_controller/tab_controller.1_test.dart',
'examples/api/test/material/selection_area/selection_area.0_test.dart',
'examples/api/test/material/scaffold/scaffold.end_drawer.0_test.dart',
'examples/api/test/material/scaffold/scaffold.drawer.0_test.dart',
'examples/api/test/material/scaffold/scaffold.1_test.dart',
'examples/api/test/material/scaffold/scaffold.of.0_test.dart',
'examples/api/test/material/scaffold/scaffold_messenger.of.0_test.dart',
'examples/api/test/material/scaffold/scaffold_messenger.0_test.dart',
'examples/api/test/material/scaffold/scaffold.0_test.dart',
'examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.0_test.dart',
'examples/api/test/material/scaffold/scaffold.2_test.dart',
'examples/api/test/material/scaffold/scaffold_messenger_state.show_material_banner.0_test.dart',
'examples/api/test/material/scaffold/scaffold.of.1_test.dart',
'examples/api/test/material/scaffold/scaffold_messenger.of.1_test.dart',
'examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.0_test.dart',
'examples/api/test/material/segmented_button/segmented_button.0_test.dart',
'examples/api/test/material/app_bar/app_bar.2_test.dart',
'examples/api/test/material/app_bar/sliver_app_bar.1_test.dart',
'examples/api/test/material/app_bar/sliver_app_bar.2_test.dart',
'examples/api/test/material/app_bar/sliver_app_bar.3_test.dart',
'examples/api/test/material/app_bar/app_bar.1_test.dart',
'examples/api/test/material/app_bar/sliver_app_bar.4_test.dart',
'examples/api/test/material/app_bar/app_bar.3_test.dart',
'examples/api/test/material/app_bar/app_bar.0_test.dart',
'examples/api/test/material/ink_well/ink_well.0_test.dart',
'examples/api/test/material/banner/material_banner.1_test.dart',
'examples/api/test/material/banner/material_banner.0_test.dart',
'examples/api/test/material/checkbox/checkbox.1_test.dart',
'examples/api/test/material/checkbox/checkbox.0_test.dart',
'examples/api/test/material/navigation_rail/navigation_rail.extended_animation.0_test.dart',
'examples/api/test/material/text_button/text_button.0_test.dart',
'examples/api/test/rendering/growth_direction/growth_direction.0_test.dart',
'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart',
'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart',
'examples/api/test/rendering/scroll_direction/scroll_direction.0_test.dart',
'examples/api/test/painting/axis_direction/axis_direction.0_test.dart',
'examples/api/test/painting/linear_border/linear_border.0_test.dart',
'examples/api/test/painting/gradient/linear_gradient.0_test.dart',
'examples/api/test/painting/star_border/star_border.0_test.dart',
'examples/api/test/painting/borders/border_side.stroke_align.0_test.dart',
'examples/api/test/widgets/autocomplete/raw_autocomplete.focus_node.0_test.dart',
'examples/api/test/widgets/autocomplete/raw_autocomplete.2_test.dart',
'examples/api/test/widgets/autocomplete/raw_autocomplete.1_test.dart',
'examples/api/test/widgets/autocomplete/raw_autocomplete.0_test.dart',
'examples/api/test/widgets/navigator/navigator.restorable_push_and_remove_until.0_test.dart',
'examples/api/test/widgets/navigator/navigator.0_test.dart',
'examples/api/test/widgets/navigator/navigator.restorable_push.0_test.dart',
'examples/api/test/widgets/navigator/navigator_state.restorable_push_replacement.0_test.dart',
'examples/api/test/widgets/navigator/navigator_state.restorable_push_and_remove_until.0_test.dart',
'examples/api/test/widgets/navigator/navigator.restorable_push_replacement.0_test.dart',
'examples/api/test/widgets/navigator/restorable_route_future.0_test.dart',
'examples/api/test/widgets/navigator/navigator_state.restorable_push.0_test.dart',
'examples/api/test/widgets/focus_manager/focus_node.unfocus.0_test.dart',
'examples/api/test/widgets/focus_manager/focus_node.0_test.dart',
'examples/api/test/widgets/framework/build_owner.0_test.dart',
'examples/api/test/widgets/framework/error_widget.0_test.dart',
'examples/api/test/widgets/inherited_theme/inherited_theme.0_test.dart',
'examples/api/test/widgets/sliver/decorated_sliver.0_test.dart',
'examples/api/test/widgets/autofill/autofill_group.0_test.dart',
'examples/api/test/widgets/drag_target/draggable.0_test.dart',
'examples/api/test/widgets/shared_app_data/shared_app_data.1_test.dart',
'examples/api/test/widgets/shared_app_data/shared_app_data.0_test.dart',
'examples/api/test/widgets/form/form.0_test.dart',
'examples/api/test/widgets/nested_scroll_view/nested_scroll_view_state.0_test.dart',
'examples/api/test/widgets/nested_scroll_view/nested_scroll_view.2_test.dart',
'examples/api/test/widgets/nested_scroll_view/nested_scroll_view.1_test.dart',
'examples/api/test/widgets/nested_scroll_view/nested_scroll_view.0_test.dart',
'examples/api/test/widgets/page_view/page_view.0_test.dart',
'examples/api/test/widgets/scroll_position/scroll_metrics_notification.0_test.dart',
'examples/api/test/widgets/media_query/media_query_data.system_gesture_insets.0_test.dart',
'examples/api/test/widgets/async/stream_builder.0_test.dart',
'examples/api/test/widgets/async/future_builder.0_test.dart',
'examples/api/test/widgets/restoration_properties/restorable_value.0_test.dart',
'examples/api/test/widgets/animated_size/animated_size.0_test.dart',
'examples/api/test/widgets/table/table.0_test.dart',
'examples/api/test/widgets/animated_switcher/animated_switcher.0_test.dart',
'examples/api/test/widgets/transitions/relative_positioned_transition.0_test.dart',
'examples/api/test/widgets/transitions/positioned_transition.0_test.dart',
'examples/api/test/widgets/transitions/listenable_builder.3_test.dart',
'examples/api/test/widgets/transitions/sliver_fade_transition.0_test.dart',
'examples/api/test/widgets/transitions/align_transition.0_test.dart',
'examples/api/test/widgets/transitions/fade_transition.0_test.dart',
'examples/api/test/widgets/transitions/animated_builder.0_test.dart',
'examples/api/test/widgets/transitions/rotation_transition.0_test.dart',
'examples/api/test/widgets/transitions/animated_widget.0_test.dart',
'examples/api/test/widgets/transitions/slide_transition.0_test.dart',
'examples/api/test/widgets/transitions/listenable_builder.2_test.dart',
'examples/api/test/widgets/transitions/scale_transition.0_test.dart',
'examples/api/test/widgets/transitions/default_text_style_transition.0_test.dart',
'examples/api/test/widgets/transitions/decorated_box_transition.0_test.dart',
'examples/api/test/widgets/transitions/size_transition.0_test.dart',
'examples/api/test/widgets/animated_list/animated_list.0_test.dart',
'examples/api/test/widgets/focus_traversal/focus_traversal_group.0_test.dart',
'examples/api/test/widgets/focus_traversal/ordered_traversal_policy.0_test.dart',
'examples/api/test/widgets/image/image.error_builder.0_test.dart',
'examples/api/test/widgets/image/image.frame_builder.0_test.dart',
'examples/api/test/widgets/image/image.loading_builder.0_test.dart',
'examples/api/test/widgets/shortcuts/logical_key_set.0_test.dart',
'examples/api/test/widgets/shortcuts/shortcuts.0_test.dart',
'examples/api/test/widgets/shortcuts/single_activator.single_activator.0_test.dart',
'examples/api/test/widgets/shortcuts/shortcuts.1_test.dart',
'examples/api/test/widgets/shortcuts/character_activator.0_test.dart',
'examples/api/test/widgets/shortcuts/callback_shortcuts.0_test.dart',
'examples/api/test/widgets/page_storage/page_storage.0_test.dart',
'examples/api/test/widgets/scrollbar/raw_scrollbar.1_test.dart',
'examples/api/test/widgets/scrollbar/raw_scrollbar.2_test.dart',
'examples/api/test/widgets/scrollbar/raw_scrollbar.desktop.0_test.dart',
'examples/api/test/widgets/scrollbar/raw_scrollbar.shape.0_test.dart',
'examples/api/test/widgets/scrollbar/raw_scrollbar.0_test.dart',
'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.2_test.dart',
'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.1_test.dart',
'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.3_test.dart',
'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.0_test.dart',
'examples/api/test/widgets/interactive_viewer/interactive_viewer.constrained.0_test.dart',
'examples/api/test/widgets/interactive_viewer/interactive_viewer.transformation_controller.0_test.dart',
'examples/api/test/widgets/interactive_viewer/interactive_viewer.0_test.dart',
'examples/api/test/widgets/notification_listener/notification.0_test.dart',
'examples/api/test/widgets/gesture_detector/gesture_detector.1_test.dart',
'examples/api/test/widgets/gesture_detector/gesture_detector.0_test.dart',
'examples/api/test/widgets/editable_text/text_editing_controller.0_test.dart',
'examples/api/test/widgets/editable_text/editable_text.on_changed.0_test.dart',
'examples/api/test/widgets/undo_history/undo_history_controller.0_test.dart',
'examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.1_test.dart',
'examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.0_test.dart',
'examples/api/test/widgets/tween_animation_builder/tween_animation_builder.0_test.dart',
'examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.1_test.dart',
'examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.0_test.dart',
'examples/api/test/widgets/overflow_bar/overflow_bar.0_test.dart',
'examples/api/test/widgets/restoration/restoration_mixin.0_test.dart',
'examples/api/test/widgets/actions/actions.0_test.dart',
'examples/api/test/widgets/actions/action_listener.0_test.dart',
'examples/api/test/widgets/actions/focusable_action_detector.0_test.dart',
'examples/api/test/widgets/color_filter/color_filtered.0_test.dart',
'examples/api/test/widgets/focus_scope/focus.2_test.dart',
'examples/api/test/widgets/focus_scope/focus.0_test.dart',
'examples/api/test/widgets/focus_scope/focus.1_test.dart',
'examples/api/test/widgets/focus_scope/focus_scope.0_test.dart',
'examples/api/test/widgets/implicit_animations/animated_fractionally_sized_box.0_test.dart',
'examples/api/test/widgets/implicit_animations/animated_align.0_test.dart',
'examples/api/test/widgets/implicit_animations/animated_positioned.0_test.dart',
'examples/api/test/widgets/implicit_animations/animated_padding.0_test.dart',
'examples/api/test/widgets/implicit_animations/sliver_animated_opacity.0_test.dart',
'examples/api/test/widgets/implicit_animations/animated_container.0_test.dart',
'examples/api/test/widgets/dismissible/dismissible.0_test.dart',
'examples/api/test/widgets/scroll_view/custom_scroll_view.1_test.dart',
'examples/api/test/widgets/preferred_size/preferred_size.0_test.dart',
'examples/api/test/widgets/inherited_notifier/inherited_notifier.0_test.dart',
'examples/api/test/animation/curves/curve2_d.0_test.dart',
'examples/api/test/gestures/pointer_signal_resolver/pointer_signal_resolver.0_test.dart',
};
......@@ -8,6 +8,7 @@ dependencies:
args: 2.4.2
crypto: 3.0.3
intl: 0.18.1
file: 6.1.4
flutter_devicelab:
path: ../devicelab
http_parser: 4.0.2
......@@ -29,7 +30,6 @@ dependencies:
convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
dart_internal: 0.2.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
frontend_server_client: 3.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
gcloud: 0.8.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
glob: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
......
// 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 'dart:io';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:path/path.dart' as path;
import '../check_code_samples.dart';
import '../utils.dart';
import 'common.dart';
void main() {
late SampleChecker checker;
late FileSystem fs;
late Directory examples;
late Directory packages;
late Directory dartUIPath;
late Directory flutterRoot;
String getRelativePath(File file, [Directory? from]) {
from ??= flutterRoot;
return path.relative(file.absolute.path, from: flutterRoot.absolute.path);
}
void writeLink({required File source, required File example}) {
source
..createSync(recursive: true)
..writeAsStringSync('''
/// Class documentation
///
/// {@tool dartpad}
/// Example description
///
/// ** See code in ${getRelativePath(example)} **
/// {@end-tool}
''');
}
void buildTestFiles({bool missingLinks = false, bool missingTests = false}) {
final Directory examplesLib = examples.childDirectory('lib').childDirectory('layer')..createSync(recursive: true);
final File fooExample = examplesLib.childFile('foo_example.0.dart')
..createSync(recursive: true)
..writeAsStringSync('// Example for foo');
final File barExample = examplesLib.childFile('bar_example.0.dart')
..createSync(recursive: true)
..writeAsStringSync('// Example for bar');
final File curvesExample =
examples.childDirectory('lib').childDirectory('animation').childDirectory('curves').childFile('curve2_d.0.dart')
..createSync(recursive: true)
..writeAsStringSync('// Missing a test, but OK');
if (missingLinks) {
examplesLib.childFile('missing_example.0.dart')
..createSync(recursive: true)
..writeAsStringSync('// Example that is not linked');
}
final Directory examplesTests = examples.childDirectory('test').childDirectory('layer')
..createSync(recursive: true);
examplesTests.childFile('foo_example.0_test.dart')
..createSync(recursive: true)
..writeAsStringSync('// test for foo example');
if (!missingTests) {
examplesTests.childFile('bar_example.0_test.dart')
..createSync(recursive: true)
..writeAsStringSync('// test for bar example');
}
if (missingLinks) {
examplesTests.childFile('missing_example.0_test.dart')
..createSync(recursive: true)
..writeAsStringSync('// test for foo example');
}
final Directory flutterPackage = packages.childDirectory('flutter').childDirectory('lib').childDirectory('src')
..createSync(recursive: true);
writeLink(source: flutterPackage.childDirectory('layer').childFile('foo.dart'), example: fooExample);
writeLink(source: flutterPackage.childDirectory('layer').childFile('bar.dart'), example: barExample);
writeLink(source: flutterPackage.childDirectory('animation').childFile('curves.dart'), example: curvesExample);
}
setUp(() {
fs = MemoryFileSystem(style: Platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix);
// Get the root prefix of the current directory so that on Windows we get a
// correct root prefix.
flutterRoot = fs.directory(path.join(path.rootPrefix(fs.currentDirectory.absolute.path), 'flutter sdk'))..createSync(recursive: true);
fs.currentDirectory = flutterRoot;
examples = flutterRoot.childDirectory('examples').childDirectory('api')..createSync(recursive: true);
packages = flutterRoot.childDirectory('packages')..createSync(recursive: true);
dartUIPath = flutterRoot
.childDirectory('bin')
.childDirectory('cache')
.childDirectory('pkg')
.childDirectory('sky_engine')
.childDirectory('lib')
..createSync(recursive: true);
checker = SampleChecker(
examples: examples,
packages: packages,
dartUIPath: dartUIPath,
flutterRoot: flutterRoot,
filesystem: fs,
);
});
test('check_code_samples.dart - checkCodeSamples catches missing links', () async {
buildTestFiles(missingLinks: true);
bool? success;
final String result = await capture(
() async {
success = checker.checkCodeSamples();
},
shouldHaveErrors: true,
);
final String lines = <String>[
'╔═╡ERROR╞═══════════════════════════════════════════════════════════════════════',
'║ The following examples are not linked from any source file API doc comments:',
'║ examples/api/lib/layer/missing_example.0.dart',
'║ Either link them to a source file API doc comment, or remove them.',
'╚═══════════════════════════════════════════════════════════════════════════════',
].map((String line) {
return line.replaceAll('/', Platform.isWindows ? r'\' : '/');
}).join('\n');
expect(result, equals('$lines\n'));
expect(success, equals(false));
});
test('check_code_samples.dart - checkCodeSamples catches missing tests', () async {
buildTestFiles(missingTests: true);
bool? success;
final String result = await capture(
() async {
success = checker.checkCodeSamples();
},
shouldHaveErrors: true,
);
final String lines = <String>[
'╔═╡ERROR╞═══════════════════════════════════════════════════════════════════════',
'║ The following example test files are missing:',
'║ examples/api/test/layer/bar_example.0_test.dart',
'╚═══════════════════════════════════════════════════════════════════════════════',
].map((String line) {
return line.replaceAll('/', Platform.isWindows ? r'\' : '/');
}).join('\n');
expect(result, equals('$lines\n'));
expect(success, equals(false));
});
test('check_code_samples.dart - checkCodeSamples succeeds', () async {
buildTestFiles();
bool? success;
final String result = await capture(
() async {
success = checker.checkCodeSamples();
},
);
expect(result, isEmpty);
expect(success, equals(true));
});
}
typedef AsyncVoidCallback = Future<void> Function();
Future<String> capture(AsyncVoidCallback callback, {bool shouldHaveErrors = false}) async {
final StringBuffer buffer = StringBuffer();
final PrintCallback oldPrint = print;
try {
print = (Object? line) {
buffer.writeln(line);
};
await callback();
expect(
hasError,
shouldHaveErrors,
reason: buffer.isEmpty
? '(No output to report.)'
: hasError
? 'Unexpected errors:\n$buffer'
: 'Unexpected success:\n$buffer',
);
} finally {
print = oldPrint;
resetErrorStatus();
}
if (stdout.supportsAnsiEscapes) {
// Remove ANSI escapes when this test is running on a terminal.
return buffer.toString().replaceAll(RegExp(r'(\x9B|\x1B\[)[0-?]{1,3}[ -/]*[@-~]'), '');
} else {
return buffer.toString();
}
}
......@@ -44,6 +44,12 @@ enum _CheckboxType { material, adaptive }
/// ** See code in examples/api/lib/material/checkbox/checkbox.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example shows what the checkbox error state looks like.
///
/// ** See code in examples/api/lib/material/checkbox/checkbox.1.dart **
/// {@end-tool}
///
/// See also:
///
/// * [CheckboxListTile], which combines this widget with a [ListTile] so that
......
......@@ -90,50 +90,13 @@ const Duration _kBaseSettleDuration = Duration(milliseconds: 246);
/// The [Drawer] displays the four items using a [ListView], which allows the
/// user to scroll through the items if need be.
///
/// ```dart
/// Scaffold(
/// appBar: AppBar(
/// title: const Text('Drawer Demo'),
/// ),
/// drawer: Drawer(
/// child: ListView(
/// padding: EdgeInsets.zero,
/// children: const <Widget>[
/// DrawerHeader(
/// decoration: BoxDecoration(
/// color: Colors.blue,
/// ),
/// child: Text(
/// 'Drawer Header',
/// style: TextStyle(
/// color: Colors.white,
/// fontSize: 24,
/// ),
/// ),
/// ),
/// ListTile(
/// leading: Icon(Icons.message),
/// title: Text('Messages'),
/// ),
/// ListTile(
/// leading: Icon(Icons.account_circle),
/// title: Text('Profile'),
/// ),
/// ListTile(
/// leading: Icon(Icons.settings),
/// title: Text('Settings'),
/// ),
/// ],
/// ),
/// ),
/// )
/// ```
/// ** See code in examples/api/lib/material/drawer/drawer.0.dart **
/// {@end-tool}
///
/// {@tool snippet}
/// This example shows how to migrate the above [Drawer] to a [NavigationDrawer].
///
/// See code in examples/api/lib/material/navigation_drawer/navigation_drawer.1.dart **
/// ** See code in examples/api/lib/material/navigation_drawer/navigation_drawer.1.dart **
/// {@end-tool}
///
/// An open drawer may be closed with a swipe to close gesture, pressing the
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment