Commit aff4e828 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Technical Debt tracker (#7667)

parent 576b4e11
// Copyright 2017 The Chromium 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:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:path/path.dart' as path;
// the numbers below are odd, so that the totals don't seem round. :-)
const double todoCost = 1009.0; // about two average SWE days, in dollars
const double ignoreCost = 2003.0; // four average SWE days, in dollars
const double pythonCost = 3001.0; // six average SWE days, in dollars
final RegExp todoPattern = new RegExp(r'(?://|#) *TODO');
final RegExp ignorePattern = new RegExp(r'// *ignore:');
Stream<double> findCostsForFile(File file) {
if (path.extension(file.path) == '.py')
return new Stream<double>.fromIterable(<double>[pythonCost]);
if (path.extension(file.path) != '.dart' &&
path.extension(file.path) != '.yaml' &&
path.extension(file.path) != '.sh')
return null;
StreamController<double> result = new StreamController<double>();
file.openRead().transform(UTF8.decoder).transform(const LineSplitter()).listen((String line) {
if (line.contains(todoPattern))
result.add(todoCost);
if (line.contains(ignorePattern))
result.add(ignoreCost);
}, onDone: () { result.close(); });
return result.stream;
}
Stream<double> findCostsForDirectory(Directory directory, Set<String> gitFiles) {
StreamController<double> result = new StreamController<double>();
Set<StreamSubscription<dynamic>> subscriptions = new Set<StreamSubscription<dynamic>>();
void checkDone(StreamSubscription<dynamic> subscription, String path) {
subscriptions.remove(subscription);
if (subscriptions.isEmpty)
result.close();
}
StreamSubscription<FileSystemEntity> listSubscription;
subscriptions.add(listSubscription = directory.list(followLinks: false).listen((FileSystemEntity entity) {
String name = path.relative(entity.path, from: flutterDirectory.path);
if (gitFiles.contains(name)) {
if (entity is File) {
StreamSubscription<double> subscription;
subscription = findCostsForFile(entity)?.listen((double cost) {
result.add(cost);
}, onDone: () { checkDone(subscription, name); });
if (subscription != null)
subscriptions.add(subscription);
} else if (entity is Directory) {
StreamSubscription<double> subscription;
subscription = findCostsForDirectory(entity, gitFiles)?.listen((double cost) {
result.add(cost);
}, onDone: () { checkDone(subscription, name); });
if (subscription != null)
subscriptions.add(subscription);
}
}
}, onDone: () { checkDone(listSubscription, directory.path); }));
return result.stream;
}
const String _kBenchmarkKey = 'technical_debt_in_dollars';
Future<Null> main() async {
await task(() async {
Process git = await startProcess(
'git',
<String>['ls-files', '--full-name', flutterDirectory.path],
workingDirectory: flutterDirectory.path,
);
Set<String> gitFiles = new Set<String>();
await for (String entry in git.stdout.transform(UTF8.decoder).transform(const LineSplitter())) {
String subentry = '';
for (String component in path.split(entry)) {
if (subentry.isNotEmpty)
subentry += path.separator;
subentry += component;
gitFiles.add(subentry);
}
}
int gitExitCode = await git.exitCode;
if (gitExitCode != 0)
throw new Exception('git exit with unexpected error code $gitExitCode');
List<double> costs = await findCostsForDirectory(flutterDirectory, gitFiles).toList();
double total = costs.fold(0.0, (double total, double cost) => total + cost);
return new TaskResult.success(
<String, dynamic>{_kBenchmarkKey: total},
benchmarkScoreKeys: <String>[_kBenchmarkKey],
);
});
}
...@@ -246,13 +246,13 @@ class AndroidDevice implements Device { ...@@ -246,13 +246,13 @@ class AndroidDevice implements Device {
} }
/// Executes [command] on `adb shell` and returns its exit code. /// Executes [command] on `adb shell` and returns its exit code.
Future<Null> shellExec(String command, List<String> arguments, {Map<String, String> env}) async { Future<Null> shellExec(String command, List<String> arguments, { Map<String, String> environment }) async {
await exec(adbPath, <String>['shell', command]..addAll(arguments), env: env, canFail: false); await exec(adbPath, <String>['shell', command]..addAll(arguments), environment: environment, canFail: false);
} }
/// Executes [command] on `adb shell` and returns its standard output as a [String]. /// Executes [command] on `adb shell` and returns its standard output as a [String].
Future<String> shellEval(String command, List<String> arguments, {Map<String, String> env}) { Future<String> shellEval(String command, List<String> arguments, { Map<String, String> environment }) {
return eval(adbPath, <String>['shell', command]..addAll(arguments), env: env, canFail: false); return eval(adbPath, <String>['shell', command]..addAll(arguments), environment: environment, canFail: false);
} }
@override @override
......
...@@ -157,20 +157,28 @@ Future<DateTime> getFlutterRepoCommitTimestamp(String commit) { ...@@ -157,20 +157,28 @@ Future<DateTime> getFlutterRepoCommitTimestamp(String commit) {
}); });
} }
Future<Process> startProcess(String executable, List<String> arguments, Future<Process> startProcess(
{Map<String, String> env}) async { String executable,
List<String> arguments, {
Map<String, String> environment,
String workingDirectory,
}) async {
String command = '$executable ${arguments?.join(" ") ?? ""}'; String command = '$executable ${arguments?.join(" ") ?? ""}';
print('Executing: $command'); print('Executing: $command');
Process proc = await Process.start(executable, arguments, Process process = await Process.start(
environment: env, workingDirectory: cwd); executable,
ProcessInfo procInfo = new ProcessInfo(command, proc); arguments,
_runningProcesses.add(procInfo); environment: environment,
workingDirectory: workingDirectory ?? cwd,
proc.exitCode.whenComplete(() { );
_runningProcesses.remove(procInfo); ProcessInfo processInfo = new ProcessInfo(command, process);
_runningProcesses.add(processInfo);
process.exitCode.whenComplete(() {
_runningProcesses.remove(processInfo);
}); });
return proc; return process;
} }
Future<Null> forceQuitRunningProcesses() async { Future<Null> forceQuitRunningProcesses() async {
...@@ -191,20 +199,24 @@ Future<Null> forceQuitRunningProcesses() async { ...@@ -191,20 +199,24 @@ Future<Null> forceQuitRunningProcesses() async {
} }
/// Executes a command and returns its exit code. /// Executes a command and returns its exit code.
Future<int> exec(String executable, List<String> arguments, Future<int> exec(
{Map<String, String> env, bool canFail: false}) async { String executable,
Process proc = await startProcess(executable, arguments, env: env); List<String> arguments, {
Map<String, String> environment,
proc.stdout bool canFail: false,
}) async {
Process process = await startProcess(executable, arguments, environment: environment);
process.stdout
.transform(UTF8.decoder) .transform(UTF8.decoder)
.transform(const LineSplitter()) .transform(const LineSplitter())
.listen(print); .listen(print);
proc.stderr process.stderr
.transform(UTF8.decoder) .transform(UTF8.decoder)
.transform(const LineSplitter()) .transform(const LineSplitter())
.listen(stderr.writeln); .listen(stderr.writeln);
int exitCode = await proc.exitCode; int exitCode = await process.exitCode;
if (exitCode != 0 && !canFail) if (exitCode != 0 && !canFail)
fail('Executable failed with exit code $exitCode.'); fail('Executable failed with exit code $exitCode.');
...@@ -215,14 +227,18 @@ Future<int> exec(String executable, List<String> arguments, ...@@ -215,14 +227,18 @@ Future<int> exec(String executable, List<String> arguments,
/// Executes a command and returns its standard output as a String. /// Executes a command and returns its standard output as a String.
/// ///
/// Standard error is redirected to the current process' standard error stream. /// Standard error is redirected to the current process' standard error stream.
Future<String> eval(String executable, List<String> arguments, Future<String> eval(
{Map<String, String> env, bool canFail: false}) async { String executable,
Process proc = await startProcess(executable, arguments, env: env); List<String> arguments, {
proc.stderr.listen((List<int> data) { Map<String, String> environment,
bool canFail: false,
}) async {
Process process = await startProcess(executable, arguments, environment: environment);
process.stderr.listen((List<int> data) {
stderr.add(data); stderr.add(data);
}); });
String output = await UTF8.decodeStream(proc.stdout); String output = await UTF8.decodeStream(process.stdout);
int exitCode = await proc.exitCode; int exitCode = await process.exitCode;
if (exitCode != 0 && !canFail) if (exitCode != 0 && !canFail)
fail('Executable failed with exit code $exitCode.'); fail('Executable failed with exit code $exitCode.');
...@@ -230,19 +246,25 @@ Future<String> eval(String executable, List<String> arguments, ...@@ -230,19 +246,25 @@ Future<String> eval(String executable, List<String> arguments,
return output.trimRight(); return output.trimRight();
} }
Future<int> flutter(String command, Future<int> flutter(String command, {
{List<String> options: const <String>[], bool canFail: false, Map<String, String> env}) { List<String> options: const <String>[],
bool canFail: false,
Map<String, String> environment,
}) {
List<String> args = <String>[command]..addAll(options); List<String> args = <String>[command]..addAll(options);
return exec(path.join(flutterDirectory.path, 'bin', 'flutter'), args, return exec(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
canFail: canFail, env: env); canFail: canFail, environment: environment);
} }
/// Runs a `flutter` command and returns the standard output as a string. /// Runs a `flutter` command and returns the standard output as a string.
Future<String> evalFlutter(String command, Future<String> evalFlutter(String command, {
{List<String> options: const <String>[], bool canFail: false, Map<String, String> env}) { List<String> options: const <String>[],
bool canFail: false,
Map<String, String> environment,
}) {
List<String> args = <String>[command]..addAll(options); List<String> args = <String>[command]..addAll(options);
return eval(path.join(flutterDirectory.path, 'bin', 'flutter'), args, return eval(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
canFail: canFail, env: env); canFail: canFail, environment: environment);
} }
String get dartBin => String get dartBin =>
......
...@@ -47,9 +47,14 @@ TaskFunction createMicrobenchmarkTask() { ...@@ -47,9 +47,14 @@ TaskFunction createMicrobenchmarkTask() {
}; };
} }
Future<Process> _startFlutter({String command = 'run', List<String> options: const <String>[], bool canFail: false, Map<String, String> env}) { Future<Process> _startFlutter({
String command = 'run',
List<String> options: const <String>[],
bool canFail: false,
Map<String, String> environment,
}) {
List<String> args = <String>['run']..addAll(options); List<String> args = <String>['run']..addAll(options);
return startProcess(path.join(flutterDirectory.path, 'bin', 'flutter'), args, env: env); return startProcess(path.join(flutterDirectory.path, 'bin', 'flutter'), args, environment: environment);
} }
Future<Map<String, double>> _readJsonResults(Process process) { Future<Map<String, double>> _readJsonResults(Process process) {
......
...@@ -255,7 +255,7 @@ class MemoryTest { ...@@ -255,7 +255,7 @@ class MemoryTest {
'-d', '-d',
deviceId, deviceId,
'--use-existing-app', '--use-existing-app',
], env: <String, String> { ], environment: <String, String> {
'VM_SERVICE_URL': 'http://localhost:$debugPort' 'VM_SERVICE_URL': 'http://localhost:$debugPort'
}); });
......
...@@ -68,6 +68,11 @@ tasks: ...@@ -68,6 +68,11 @@ tasks:
stage: devicelab stage: devicelab
required_agent_capabilities: ["has-android-device"] required_agent_capabilities: ["has-android-device"]
technical_debt__cost:
description: >
Estimates our technical debt (TODOs, analyzer ignores, etc).
stage: devicelab
required_agent_capabilities: ["has-android-device"]
# Android on-device tests # Android on-device tests
......
...@@ -100,23 +100,29 @@ void expectLog(List<CommandArgs> log) { ...@@ -100,23 +100,29 @@ void expectLog(List<CommandArgs> log) {
expect(FakeDevice.commandLog, log); expect(FakeDevice.commandLog, log);
} }
CommandArgs cmd({String command, List<String> arguments, Map<String, String> env}) => new CommandArgs( CommandArgs cmd({
command: command, String command,
arguments: arguments, List<String> arguments,
env: env Map<String, String> environment,
); }) {
return new CommandArgs(
command: command,
arguments: arguments,
environment: environment,
);
}
typedef dynamic ExitErrorFactory(); typedef dynamic ExitErrorFactory();
class CommandArgs { class CommandArgs {
CommandArgs({this.command, this.arguments, this.env}); CommandArgs({ this.command, this.arguments, this.environment });
final String command; final String command;
final List<String> arguments; final List<String> arguments;
final Map<String, String> env; final Map<String, String> environment;
@override @override
String toString() => 'CommandArgs(command: $command, arguments: $arguments, env: $env)'; String toString() => 'CommandArgs(command: $command, arguments: $arguments, environment: $environment)';
@override @override
bool operator==(Object other) { bool operator==(Object other) {
...@@ -126,18 +132,18 @@ class CommandArgs { ...@@ -126,18 +132,18 @@ class CommandArgs {
CommandArgs otherCmd = other; CommandArgs otherCmd = other;
return otherCmd.command == this.command && return otherCmd.command == this.command &&
const ListEquality<String>().equals(otherCmd.arguments, this.arguments) && const ListEquality<String>().equals(otherCmd.arguments, this.arguments) &&
const MapEquality<String, String>().equals(otherCmd.env, this.env); const MapEquality<String, String>().equals(otherCmd.environment, this.environment);
} }
@override @override
int get hashCode => 17 * (17 * command.hashCode + _hashArguments) + _hashEnv; int get hashCode => 17 * (17 * command.hashCode + _hashArguments) + _hashEnvironment;
int get _hashArguments => arguments != null int get _hashArguments => arguments != null
? const ListEquality<String>().hash(arguments) ? const ListEquality<String>().hash(arguments)
: null.hashCode; : null.hashCode;
int get _hashEnv => env != null int get _hashEnvironment => environment != null
? const MapEquality<String, String>().hash(env) ? const MapEquality<String, String>().hash(environment)
: null.hashCode; : null.hashCode;
} }
...@@ -166,21 +172,21 @@ class FakeDevice extends AndroidDevice { ...@@ -166,21 +172,21 @@ class FakeDevice extends AndroidDevice {
} }
@override @override
Future<String> shellEval(String command, List<String> arguments, {Map<String, String> env}) async { Future<String> shellEval(String command, List<String> arguments, { Map<String, String> environment }) async {
commandLog.add(new CommandArgs( commandLog.add(new CommandArgs(
command: command, command: command,
arguments: arguments, arguments: arguments,
env: env environment: environment,
)); ));
return output; return output;
} }
@override @override
Future<Null> shellExec(String command, List<String> arguments, {Map<String, String> env}) async { Future<Null> shellExec(String command, List<String> arguments, { Map<String, String> environment }) async {
commandLog.add(new CommandArgs( commandLog.add(new CommandArgs(
command: command, command: command,
arguments: arguments, arguments: arguments,
env: env environment: environment,
)); ));
dynamic exitError = exitErrorFactory(); dynamic exitError = exitErrorFactory();
if (exitError != null) if (exitError != null)
......
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