analyze_test.dart 17.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9
// 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:path/path.dart' as path;

import '../analyze.dart';
10 11
import '../custom_rules/analyze.dart';
import '../custom_rules/no_double_clamp.dart';
12
import '../custom_rules/no_stop_watches.dart';
13
import '../utils.dart';
14 15 16 17
import 'common.dart';

typedef AsyncVoidCallback = Future<void> Function();

18
Future<String> capture(AsyncVoidCallback callback, { bool shouldHaveErrors = false }) async {
19 20 21
  final StringBuffer buffer = StringBuffer();
  final PrintCallback oldPrint = print;
  try {
22
    print = (Object? line) {
23 24
      buffer.writeln(line);
    };
25 26 27 28 29 30
    await callback();
    expect(
      hasError,
      shouldHaveErrors,
      reason: buffer.isEmpty ? '(No output to report.)' : hasError ? 'Unexpected errors:\n$buffer' : 'Unexpected success:\n$buffer',
    );
31 32
  } finally {
    print = oldPrint;
33
    resetErrorStatus();
34
  }
35 36 37 38 39 40
  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();
  }
41 42 43
}

void main() {
44
  final String testRootPath = path.join('test', 'analyze-test-input', 'root');
45 46
  final String dartName = Platform.isWindows ? 'dart.exe' : 'dart';
  final String dartPath = path.canonicalize(path.join('..', '..', 'bin', 'cache', 'dart-sdk', 'bin', dartName));
47

48
  test('analyze.dart - verifyDeprecations', () async {
49
    final String result = await capture(() => verifyDeprecations(testRootPath, minimumMatches: 2), shouldHaveErrors: true);
50
    final String lines = <String>[
51
        '║ test/analyze-test-input/root/packages/foo/deprecation.dart:12: Deprecation notice does not match required pattern. There might be a missing space character at the end of the line.',
52
        '║ test/analyze-test-input/root/packages/foo/deprecation.dart:18: Deprecation notice should be a grammatically correct sentence and start with a capital letter; see style guide: STYLE_GUIDE_URL',
53
        '║ test/analyze-test-input/root/packages/foo/deprecation.dart:25: Deprecation notice should be a grammatically correct sentence and end with a period; notice appears to be "Also bad grammar".',
54 55
        '║ test/analyze-test-input/root/packages/foo/deprecation.dart:29: Deprecation notice does not match required pattern.',
        '║ test/analyze-test-input/root/packages/foo/deprecation.dart:32: Deprecation notice does not match required pattern.',
56 57
        '║ test/analyze-test-input/root/packages/foo/deprecation.dart:37: Deprecation notice does not match required pattern. It might be missing the line saying "This feature was deprecated after...".',
        '║ test/analyze-test-input/root/packages/foo/deprecation.dart:41: Deprecation notice does not match required pattern. There might not be an explanatory message.',
58 59 60 61 62
        '║ test/analyze-test-input/root/packages/foo/deprecation.dart:48: End of deprecation notice does not match required pattern.',
        '║ test/analyze-test-input/root/packages/foo/deprecation.dart:51: Unexpected deprecation notice indent.',
        '║ test/analyze-test-input/root/packages/foo/deprecation.dart:70: Deprecation notice does not accurately indicate a beta branch version number; please see RELEASES_URL to find the latest beta build version number.',
        '║ test/analyze-test-input/root/packages/foo/deprecation.dart:76: Deprecation notice does not accurately indicate a beta branch version number; please see RELEASES_URL to find the latest beta build version number.',
        '║ test/analyze-test-input/root/packages/foo/deprecation.dart:99: Deprecation notice does not match required pattern. You might have used double quotes (") for the string instead of single quotes (\').',
63 64 65 66 67 68 69 70
      ]
      .map((String line) {
        return line
          .replaceAll('/', Platform.isWindows ? r'\' : '/')
          .replaceAll('STYLE_GUIDE_URL', 'https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo')
          .replaceAll('RELEASES_URL', 'https://flutter.dev/docs/development/tools/sdk/releases');
      })
      .join('\n');
71
    expect(result,
72
      '╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════\n'
73
      '$lines\n'
74 75
      '║ See: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes\n'
      '╚═══════════════════════════════════════════════════════════════════════════════\n'
76 77 78
    );
  });

79
  test('analyze.dart - verifyGoldenTags', () async {
80
    final List<String> result = (await capture(() => verifyGoldenTags(testRootPath, minimumMatches: 6), shouldHaveErrors: true)).split('\n');
81
    const String noTag = "Files containing golden tests must be tagged using @Tags(<String>['reduced-test-set']) "
82
                         'at the top of the file before import statements.';
83
    const String missingTag = "Files containing golden tests must be tagged with 'reduced-test-set'.";
84 85 86
    final List<String> lines = <String>[
        '║ test/analyze-test-input/root/packages/foo/golden_missing_tag.dart: $missingTag',
        '║ test/analyze-test-input/root/packages/foo/golden_no_tag.dart: $noTag',
87
      ]
88 89 90
      .map((String line) => line.replaceAll('/', Platform.isWindows ? r'\' : '/'))
      .toList();
    expect(result.length, 4 + lines.length, reason: 'output had unexpected number of lines:\n${result.join('\n')}');
91
    expect(result[0], '╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════');
92 93 94 95
    expect(result.getRange(1, result.length - 3).toSet(), lines.toSet());
    expect(result[result.length - 3], '║ See: https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter');
    expect(result[result.length - 2], '╚═══════════════════════════════════════════════════════════════════════════════');
    expect(result[result.length - 1], ''); // trailing newline
96 97
  });

98
  test('analyze.dart - verifyNoMissingLicense', () async {
99
    final String result = await capture(() => verifyNoMissingLicense(testRootPath, checkMinimums: false), shouldHaveErrors: true);
100
    final String file = 'test/analyze-test-input/root/packages/foo/foo.dart'
101
      .replaceAll('/', Platform.isWindows ? r'\' : '/');
Ian Hickson's avatar
Ian Hickson committed
102
    expect(result,
103
      '╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════\n'
104 105 106 107 108 109 110 111
      '║ The following file does not have the right license header for dart files:\n'
      '║   $file\n'
      '║ The expected license header is:\n'
      '║ // Copyright 2014 The Flutter Authors. All rights reserved.\n'
      '║ // Use of this source code is governed by a BSD-style license that can be\n'
      '║ // found in the LICENSE file.\n'
      '║ ...followed by a blank line.\n'
      '╚═══════════════════════════════════════════════════════════════════════════════\n'
Ian Hickson's avatar
Ian Hickson committed
112
    );
113
  });
114 115

  test('analyze.dart - verifyNoTrailingSpaces', () async {
116
    final String result = await capture(() => verifyNoTrailingSpaces(testRootPath, minimumMatches: 2), shouldHaveErrors: true);
117
    final String lines = <String>[
118 119
        '║ test/analyze-test-input/root/packages/foo/spaces.txt:5: trailing U+0020 space character',
        '║ test/analyze-test-input/root/packages/foo/spaces.txt:9: trailing blank line',
120 121 122
      ]
      .map((String line) => line.replaceAll('/', Platform.isWindows ? r'\' : '/'))
      .join('\n');
123
    expect(result,
124
      '╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════\n'
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
      '$lines\n'
      '╚═══════════════════════════════════════════════════════════════════════════════\n'
    );
  });

  test('analyze.dart - verifySpacesAfterFlowControlStatements', () async {
    final String result = await capture(() => verifySpacesAfterFlowControlStatements(testRootPath, minimumMatches: 2), shouldHaveErrors: true);
    final String lines = <String>[
        '║ test/analyze-test-input/root/packages/foo/spaces_after_flow.dart:11: no space after flow control statement',
        '║ test/analyze-test-input/root/packages/foo/spaces_after_flow.dart:18: no space after flow control statement',
        '║ test/analyze-test-input/root/packages/foo/spaces_after_flow.dart:25: no space after flow control statement',
        '║ test/analyze-test-input/root/packages/foo/spaces_after_flow.dart:29: no space after flow control statement',
        '║ test/analyze-test-input/root/packages/foo/spaces_after_flow.dart:35: no space after flow control statement',
      ]
      .map((String line) => line.replaceAll('/', Platform.isWindows ? r'\' : '/'))
      .join('\n');
    expect(result,
142
      '╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════\n'
143
      '$lines\n'
144
      '╚═══════════════════════════════════════════════════════════════════════════════\n'
145 146 147 148 149 150
    );
  });

  test('analyze.dart - verifyNoBinaries - positive', () async {
    final String result = await capture(() => verifyNoBinaries(
      testRootPath,
151
      legacyBinaries: <Hash256>{const Hash256(0x39A050CD69434936, 0, 0, 0)},
152
    ), shouldHaveErrors: !Platform.isWindows);
153
    if (!Platform.isWindows) {
154
      expect(result,
155
        '╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════\n'
156 157 158 159 160 161 162 163 164
        '║ test/analyze-test-input/root/packages/foo/serviceaccount.enc:0: file is not valid UTF-8\n'
        '║ All files in this repository must be UTF-8. In particular, images and other binaries\n'
        '║ must not be checked into this repository. This is because we are very sensitive to the\n'
        '║ size of the repository as it is distributed to all our developers. If you have a binary\n'
        '║ to which you need access, you should consider how to fetch it from another repository;\n'
        '║ for example, the "assets-for-api-docs" repository is used for images in API docs.\n'
        '║ To add assets to flutter_tools templates, see the instructions in the wiki:\n'
        '║ https://github.com/flutter/flutter/wiki/Managing-template-image-assets\n'
        '╚═══════════════════════════════════════════════════════════════════════════════\n'
165
      );
166 167 168
    }
  });

169
  test('analyze.dart - verifyInternationalizations - comparison fails', () async {
170
    final String result = await capture(() => verifyInternationalizations(testRootPath, dartPath), shouldHaveErrors: true);
171 172 173 174 175 176 177 178 179 180 181 182 183
    final String genLocalizationsScript = path.join('dev', 'tools', 'localization', 'bin', 'gen_localizations.dart');
    expect(result,
        contains('$dartName $genLocalizationsScript --cupertino'));
    expect(result,
        contains('$dartName $genLocalizationsScript --material'));
    final String generatedFile = path.join(testRootPath, 'packages', 'flutter_localizations',
        'lib', 'src', 'l10n', 'generated_material_localizations.dart');
    expect(result,
        contains('The contents of $generatedFile are different from that produced by gen_localizations.'));
    expect(result,
        contains(r'Did you forget to run gen_localizations.dart after updating a .arb file?'));
  });

184 185 186
  test('analyze.dart - verifyNoBinaries - negative', () async {
    await capture(() => verifyNoBinaries(
      testRootPath,
187
      legacyBinaries: <Hash256>{
188 189
        const Hash256(0xA8100AE6AA1940D0, 0xB663BB31CD466142, 0xEBBDBD5187131B92, 0xD93818987832EB89), // sha256("\xff")
        const Hash256(0x155644D3F13D98BF, 0, 0, 0),
190
      },
191
    ));
192
  });
193 194 195 196 197

  test('analyze.dart - verifyNullInitializedDebugExpensiveFields', () async {
    final String result = await capture(() => verifyNullInitializedDebugExpensiveFields(
      testRootPath,
      minimumMatches: 1,
198
    ), shouldHaveErrors: true);
199

200 201 202 203 204 205 206 207 208 209 210 211 212
    expect(result, contains(':15'));
    expect(result, isNot(contains(':12')));
  });

  test('analyze.dart - verifyTabooDocumentation', () async {
    final String result = await capture(() => verifyTabooDocumentation(
      testRootPath,
      minimumMatches: 1,
    ), shouldHaveErrors: true);

    expect(result, isNot(contains(':19')));
    expect(result, contains(':20'));
    expect(result, contains(':21'));
213
  });
214 215

  test('analyze.dart - clampDouble', () async {
216
    final String result = await capture(() => analyzeWithRules(
217 218
      testRootPath,
      <AnalyzeRule>[noDoubleClamp],
219
      includePaths: <String>['packages/flutter/lib'],
220 221 222 223 224 225 226 227 228 229 230 231
    ), shouldHaveErrors: true);
    final String lines = <String>[
        '║ packages/flutter/lib/bar.dart:37: input.clamp(0.0, 2)',
        '║ packages/flutter/lib/bar.dart:38: input.toDouble().clamp(0, 2)',
        '║ packages/flutter/lib/bar.dart:42: nullableInt?.clamp(0, 2.0)',
        '║ packages/flutter/lib/bar.dart:43: nullableDouble?.clamp(0, 2)',
        '║ packages/flutter/lib/bar.dart:48: nullableInt?.clamp',
        '║ packages/flutter/lib/bar.dart:50: nullableDouble?.clamp',
      ]
      .map((String line) => line.replaceAll('/', Platform.isWindows ? r'\' : '/'))
      .join('\n');
    expect(result,
232
      '╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════\n'
233 234 235 236 237 238
      '$lines\n'
      '║ \n'
      '║ For performance reasons, we use a custom "clampDouble" function instead of using "double.clamp".\n'
      '╚═══════════════════════════════════════════════════════════════════════════════\n'
    );
  });
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261

  test('analyze.dart - stopwatch', () async {
    final String result = await capture(() => analyzeWithRules(
      testRootPath,
      <AnalyzeRule>[noStopwatches],
      includePaths: <String>['packages/flutter/lib'],
    ), shouldHaveErrors: true);
    final String lines = <String>[
      '║ packages/flutter/lib/stopwatch.dart:18: Stopwatch()',
      '║ packages/flutter/lib/stopwatch.dart:19: Stopwatch()',
      '║ packages/flutter/lib/stopwatch.dart:24: StopwatchAtHome()',
      '║ packages/flutter/lib/stopwatch.dart:27: StopwatchAtHome.new',
      '║ packages/flutter/lib/stopwatch.dart:30: StopwatchAtHome.create',
      '║ packages/flutter/lib/stopwatch.dart:36: externallib.MyStopwatch.create()',
      '║ packages/flutter/lib/stopwatch.dart:40: externallib.MyStopwatch.new',
      '║ packages/flutter/lib/stopwatch.dart:45: externallib.stopwatch',
      '║ packages/flutter/lib/stopwatch.dart:46: externallib.createMyStopwatch()',
      '║ packages/flutter/lib/stopwatch.dart:47: externallib.createStopwatch()',
      '║ packages/flutter/lib/stopwatch.dart:48: externallib.createMyStopwatch'
    ]
      .map((String line) => line.replaceAll('/', Platform.isWindows ? r'\' : '/'))
      .join('\n');
    expect(result,
262
      '╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════\n'
263 264 265 266 267 268 269
      '$lines\n'
      '║ \n'
      '║ Stopwatches introduce flakes by falling out of sync with the FakeAsync used in testing.\n'
      '║ A Stopwatch that stays in sync with FakeAsync is available through the Gesture or Test bindings, through samplingClock.\n'
      '╚═══════════════════════════════════════════════════════════════════════════════\n'
    );
  });
270
}