blob: 73129a0f99665d1de6b76bcdda7a71c95fd6ae6e [file] [log] [blame]
// Copyright 2013 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' as io show Directory, File, Platform, stderr;
import 'package:clang_tidy/clang_tidy.dart';
import 'package:clang_tidy/src/command.dart';
import 'package:clang_tidy/src/options.dart';
import 'package:litetest/litetest.dart';
import 'package:path/path.dart' as path;
import 'package:process_runner/process_runner.dart';
// Recorded locally from clang-tidy.
const String _tidyOutput = '''
/runtime.dart_isolate.o" in /Users/aaclarke/dev/engine/src/out/host_debug exited with code 1
3467 warnings generated.
/Users/aaclarke/dev/engine/src/flutter/runtime/dart_isolate.cc:167:32: error: std::move of the const variable 'dart_entrypoint_args' has no effect; remove std::move() or make the variable non-const [performance-move-const-arg,-warnings-as-errors]
std::move(dart_entrypoint_args))) {
^~~~~~~~~~ ~
Suppressed 3474 warnings (3466 in non-user code, 8 NOLINT).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
1 warning treated as error
:
3467 warnings generated.
Suppressed 3474 warnings (3466 in non-user code, 8 NOLINT).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
1 warning treated as error
''';
const String _tidyTrimmedOutput = '''
/Users/aaclarke/dev/engine/src/flutter/runtime/dart_isolate.cc:167:32: error: std::move of the const variable 'dart_entrypoint_args' has no effect; remove std::move() or make the variable non-const [performance-move-const-arg,-warnings-as-errors]
std::move(dart_entrypoint_args))) {
^~~~~~~~~~ ~
Suppressed 3474 warnings (3466 in non-user code, 8 NOLINT).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
1 warning treated as error''';
void _withTempFile(String prefix, void Function(String path) func) {
final String filePath =
path.join(io.Directory.systemTemp.path, '$prefix-temp-file');
final io.File file = io.File(filePath);
file.createSync();
try {
func(file.path);
} finally {
file.deleteSync();
}
}
Future<int> main(List<String> args) async {
if (args.isEmpty) {
io.stderr.writeln(
'Usage: clang_tidy_test.dart [path/to/compile_commands.json]',
);
return 1;
}
final String buildCommands = args[0];
test('--help gives help', () async {
final StringBuffer outBuffer = StringBuffer();
final StringBuffer errBuffer = StringBuffer();
final ClangTidy clangTidy = ClangTidy.fromCommandLine(
<String>[
'--help',
],
outSink: outBuffer,
errSink: errBuffer,
);
final int result = await clangTidy.run();
expect(clangTidy.options.help, isTrue);
expect(result, equals(0));
expect(errBuffer.toString(), contains('Usage: '));
});
test('trimmed clang-tidy output', () {
expect(_tidyTrimmedOutput, equals(ClangTidy.trimOutput(_tidyOutput)));
});
test('Error when --compile-commands and --target-variant are used together', () async {
final StringBuffer outBuffer = StringBuffer();
final StringBuffer errBuffer = StringBuffer();
final ClangTidy clangTidy = ClangTidy.fromCommandLine(
<String>[
'--compile-commands',
'/unused',
'--target-variant',
'unused'
],
outSink: outBuffer,
errSink: errBuffer,
);
final int result = await clangTidy.run();
expect(clangTidy.options.help, isFalse);
expect(result, equals(1));
expect(errBuffer.toString(), contains(
'ERROR: --compile-commands option cannot be used with --target-variant.',
));
});
test('Error when --compile-commands and --src-dir are used together', () async {
final StringBuffer outBuffer = StringBuffer();
final StringBuffer errBuffer = StringBuffer();
final ClangTidy clangTidy = ClangTidy.fromCommandLine(
<String>[
'--compile-commands',
'/unused',
'--src-dir',
'/unused',
],
outSink: outBuffer,
errSink: errBuffer,
);
final int result = await clangTidy.run();
expect(clangTidy.options.help, isFalse);
expect(result, equals(1));
expect(errBuffer.toString(), contains(
'ERROR: --compile-commands option cannot be used with --src-dir.',
));
});
test('shard-id valid', () async {
_withTempFile('shard-id-valid', (String path) {
final Options options = Options.fromCommandLine( <String>[
'--compile-commands=$path',
'--shard-variants=variant',
'--shard-id=1',
],);
expect(options.errorMessage, isNull);
expect(options.shardId, equals(1));
});
});
test('shard-id invalid', () async {
_withTempFile('shard-id-valid', (String path) {
final StringBuffer errBuffer = StringBuffer();
final Options options = Options.fromCommandLine(<String>[
'--compile-commands=$path',
'--shard-variants=variant',
'--shard-id=2',
], errSink: errBuffer);
expect(options.errorMessage, isNotNull);
expect(options.shardId, isNull);
print('foo ${options.errorMessage}');
expect(
options.errorMessage,
contains(
'Invalid shard-id value',
));
});
});
test('Error when --compile-commands path does not exist', () async {
final StringBuffer outBuffer = StringBuffer();
final StringBuffer errBuffer = StringBuffer();
final ClangTidy clangTidy = ClangTidy.fromCommandLine(
<String>[
'--compile-commands',
'/does/not/exist',
],
outSink: outBuffer,
errSink: errBuffer,
);
final int result = await clangTidy.run();
expect(clangTidy.options.help, isFalse);
expect(result, equals(1));
expect(errBuffer.toString().split('\n')[0], hasMatch(
r"ERROR: Build commands path .*/does/not/exist doesn't exist.",
));
});
test('Error when --src-dir path does not exist, uses target variant in path', () async {
final StringBuffer outBuffer = StringBuffer();
final StringBuffer errBuffer = StringBuffer();
final ClangTidy clangTidy = ClangTidy.fromCommandLine(
<String>[
'--src-dir',
'/does/not/exist',
'--target-variant',
'ios_debug_unopt',
],
outSink: outBuffer,
errSink: errBuffer,
);
final int result = await clangTidy.run();
expect(clangTidy.options.help, isFalse);
expect(result, equals(1));
expect(errBuffer.toString().split('\n')[0], hasMatch(
r'ERROR: Build commands path .*/does/not/exist'
r'[/\\]out[/\\]ios_debug_unopt[/\\]compile_commands.json'
r" doesn't exist.",
));
});
test('Error when --lint-all and --lint-head are used together', () async {
final StringBuffer outBuffer = StringBuffer();
final StringBuffer errBuffer = StringBuffer();
final ClangTidy clangTidy = ClangTidy.fromCommandLine(
<String>[
'--compile-commands',
'/unused',
'--lint-all',
'--lint-head',
],
outSink: outBuffer,
errSink: errBuffer,
);
final int result = await clangTidy.run();
expect(clangTidy.options.help, isFalse);
expect(result, equals(1));
expect(errBuffer.toString(), contains(
'ERROR: At most one of --lint-all and --lint-head can be passed.',
));
});
test('lintAll=true checks all files', () async {
final StringBuffer outBuffer = StringBuffer();
final StringBuffer errBuffer = StringBuffer();
final ClangTidy clangTidy = ClangTidy(
buildCommandsPath: io.File(buildCommands),
lintAll: true,
outSink: outBuffer,
errSink: errBuffer,
);
final List<io.File> fileList = await clangTidy.computeFilesOfInterest();
expect(fileList.length, greaterThan(1000));
});
test('lintAll=false does not check all files', () async {
final StringBuffer outBuffer = StringBuffer();
final StringBuffer errBuffer = StringBuffer();
final ClangTidy clangTidy = ClangTidy(
buildCommandsPath: io.File(buildCommands),
outSink: outBuffer,
errSink: errBuffer,
);
final List<io.File> fileList = await clangTidy.computeFilesOfInterest();
expect(fileList.length, lessThan(300));
});
test('Sharding', () async {
final StringBuffer outBuffer = StringBuffer();
final StringBuffer errBuffer = StringBuffer();
final ClangTidy clangTidy = ClangTidy(
buildCommandsPath: io.File(buildCommands),
lintAll: true,
outSink: outBuffer,
errSink: errBuffer,
);
Map<String, dynamic> makeBuildCommandEntry(String filePath) => <String, dynamic>{
'directory': '/unused',
'command': '../../buildtools/mac-x64/clang/bin/clang $filePath',
'file': filePath,
};
final List<String> filePaths = <String>[
for (int i = 0; i < 10; ++i) '/path/to/a/source_file_$i.cc'
];
final List<dynamic> buildCommandsData =
filePaths.map((String e) => makeBuildCommandEntry(e)).toList();
final List<dynamic> shardBuildCommandsData =
filePaths.sublist(6).map((String e) => makeBuildCommandEntry(e)).toList();
{
final List<Command> commands = await clangTidy.getLintCommandsForFiles(
buildCommandsData,
filePaths.map((String e) => io.File(e)).toList(),
<List<dynamic>>[shardBuildCommandsData],
0,
);
final Iterable<String> commandFilePaths = commands.map((Command e) => e.filePath);
expect(commands.length, equals(8));
expect(commandFilePaths.contains('/path/to/a/source_file_0.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_1.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_2.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_3.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_4.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_5.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_6.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_7.cc'), false);
expect(commandFilePaths.contains('/path/to/a/source_file_8.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_9.cc'), false);
}
{
final List<Command> commands = await clangTidy.getLintCommandsForFiles(
buildCommandsData,
filePaths.map((String e) => io.File(e)).toList(),
<List<dynamic>>[shardBuildCommandsData],
1,
);
final Iterable<String> commandFilePaths = commands.map((Command e) => e.filePath);
expect(commands.length, equals(8));
expect(commandFilePaths.contains('/path/to/a/source_file_0.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_1.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_2.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_3.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_4.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_5.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_6.cc'), false);
expect(commandFilePaths.contains('/path/to/a/source_file_7.cc'), true);
expect(commandFilePaths.contains('/path/to/a/source_file_8.cc'), false);
expect(commandFilePaths.contains('/path/to/a/source_file_9.cc'), true);
}
});
test('No Commands are produced when no files changed', () async {
final StringBuffer outBuffer = StringBuffer();
final StringBuffer errBuffer = StringBuffer();
final ClangTidy clangTidy = ClangTidy(
buildCommandsPath: io.File(buildCommands),
lintAll: true,
outSink: outBuffer,
errSink: errBuffer,
);
const String filePath = '/path/to/a/source_file.cc';
final List<dynamic> buildCommandsData = <Map<String, dynamic>>[
<String, dynamic>{
'directory': '/unused',
'command': '../../buildtools/mac-x64/clang/bin/clang $filePath',
'file': filePath,
},
];
final List<Command> commands = await clangTidy.getLintCommandsForFiles(
buildCommandsData,
<io.File>[],
<List<dynamic>>[],
null,
);
expect(commands, isEmpty);
});
test('A Command is produced when a file is changed', () async {
final StringBuffer outBuffer = StringBuffer();
final StringBuffer errBuffer = StringBuffer();
final ClangTidy clangTidy = ClangTidy(
buildCommandsPath: io.File(buildCommands),
lintAll: true,
outSink: outBuffer,
errSink: errBuffer,
);
// This file needs to exist, and be UTF8 line-parsable.
final String filePath = io.Platform.script.toFilePath();
final List<dynamic> buildCommandsData = <Map<String, dynamic>>[
<String, dynamic>{
'directory': '/unused',
'command': '../../buildtools/mac-x64/clang/bin/clang $filePath',
'file': filePath,
},
];
final List<Command> commands = await clangTidy.getLintCommandsForFiles(
buildCommandsData,
<io.File>[io.File(filePath)],
<List<dynamic>>[],
null,
);
expect(commands, isNotEmpty);
final Command command = commands.first;
expect(command.tidyPath, contains('clang/bin/clang-tidy'));
final Options noFixOptions = Options(buildCommandsPath: io.File('.'));
expect(noFixOptions.fix, isFalse);
final WorkerJob jobNoFix = command.createLintJob(noFixOptions);
expect(jobNoFix.command[0], endsWith('../../buildtools/mac-x64/clang/bin/clang-tidy'));
expect(jobNoFix.command[1], endsWith(filePath.replaceAll('/', io.Platform.pathSeparator)));
expect(jobNoFix.command[2], '--');
expect(jobNoFix.command[3], '');
expect(jobNoFix.command[4], endsWith(filePath));
final Options fixOptions = Options(buildCommandsPath: io.File('.'), fix: true);
final WorkerJob jobWithFix = command.createLintJob(fixOptions);
expect(jobWithFix.command[0], endsWith('../../buildtools/mac-x64/clang/bin/clang-tidy'));
expect(jobWithFix.command[1], endsWith(filePath.replaceAll('/', io.Platform.pathSeparator)));
expect(jobWithFix.command[2], '--fix');
expect(jobWithFix.command[3], '--format-style=file');
expect(jobWithFix.command[4], '--');
expect(jobWithFix.command[5], '');
expect(jobWithFix.command[6], endsWith(filePath));
});
test('Command getLintAction flags third_party files', () async {
final LintAction lintAction = await Command.getLintAction(
'/some/file/in/a/third_party/dependency',
);
expect(lintAction, equals(LintAction.skipThirdParty));
});
test('Command getLintAction flags missing files', () async {
final LintAction lintAction = await Command.getLintAction(
'/does/not/exist',
);
expect(lintAction, equals(LintAction.skipMissing));
});
test('Command getLintActionFromContents flags FLUTTER_NOLINT', () async {
final LintAction lintAction = await Command.lintActionFromContents(
Stream<String>.fromIterable(<String>[
'// Copyright 2013 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',
'\n',
'// FLUTTER_NOLINT: https://github.com/flutter/flutter/issues/68332\n',
'\n',
'#include "flutter/shell/version/version.h"\n',
]),
);
expect(lintAction, equals(LintAction.skipNoLint));
});
test('Command getLintActionFromContents flags malformed FLUTTER_NOLINT', () async {
final LintAction lintAction = await Command.lintActionFromContents(
Stream<String>.fromIterable(<String>[
'// Copyright 2013 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',
'\n',
'// FLUTTER_NOLINT: https://gir/flutter/issues/68332\n',
'\n',
'#include "flutter/shell/version/version.h"\n',
]),
);
expect(lintAction, equals(LintAction.failMalformedNoLint));
});
test('Command getLintActionFromContents flags that we should lint', () async {
final LintAction lintAction = await Command.lintActionFromContents(
Stream<String>.fromIterable(<String>[
'// Copyright 2013 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',
'\n',
'#include "flutter/shell/version/version.h"\n',
]),
);
expect(lintAction, equals(LintAction.lint));
});
return 0;
}