diff --git a/dev/devicelab/bin/tasks/technical_debt__cost.dart b/dev/devicelab/bin/tasks/technical_debt__cost.dart new file mode 100644 index 00000000000..0e468d22143 --- /dev/null +++ b/dev/devicelab/bin/tasks/technical_debt__cost.dart @@ -0,0 +1,101 @@ +// 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 findCostsForFile(File file) { + if (path.extension(file.path) == '.py') + return new Stream.fromIterable([pythonCost]); + if (path.extension(file.path) != '.dart' && + path.extension(file.path) != '.yaml' && + path.extension(file.path) != '.sh') + return null; + StreamController result = new StreamController(); + 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 findCostsForDirectory(Directory directory, Set gitFiles) { + StreamController result = new StreamController(); + Set> subscriptions = new Set>(); + + void checkDone(StreamSubscription subscription, String path) { + subscriptions.remove(subscription); + if (subscriptions.isEmpty) + result.close(); + } + + StreamSubscription 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 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 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 main() async { + await task(() async { + Process git = await startProcess( + 'git', + ['ls-files', '--full-name', flutterDirectory.path], + workingDirectory: flutterDirectory.path, + ); + Set gitFiles = new Set(); + 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 costs = await findCostsForDirectory(flutterDirectory, gitFiles).toList(); + double total = costs.fold(0.0, (double total, double cost) => total + cost); + return new TaskResult.success( + {_kBenchmarkKey: total}, + benchmarkScoreKeys: [_kBenchmarkKey], + ); + }); +} diff --git a/dev/devicelab/lib/framework/adb.dart b/dev/devicelab/lib/framework/adb.dart index ca3eefaf3c0..062ef44c320 100644 --- a/dev/devicelab/lib/framework/adb.dart +++ b/dev/devicelab/lib/framework/adb.dart @@ -246,13 +246,13 @@ class AndroidDevice implements Device { } /// Executes [command] on `adb shell` and returns its exit code. - Future shellExec(String command, List arguments, {Map env}) async { - await exec(adbPath, ['shell', command]..addAll(arguments), env: env, canFail: false); + Future shellExec(String command, List arguments, { Map environment }) async { + await exec(adbPath, ['shell', command]..addAll(arguments), environment: environment, canFail: false); } /// Executes [command] on `adb shell` and returns its standard output as a [String]. - Future shellEval(String command, List arguments, {Map env}) { - return eval(adbPath, ['shell', command]..addAll(arguments), env: env, canFail: false); + Future shellEval(String command, List arguments, { Map environment }) { + return eval(adbPath, ['shell', command]..addAll(arguments), environment: environment, canFail: false); } @override diff --git a/dev/devicelab/lib/framework/utils.dart b/dev/devicelab/lib/framework/utils.dart index 3ec8b7dfbfa..9bae2ea4cde 100644 --- a/dev/devicelab/lib/framework/utils.dart +++ b/dev/devicelab/lib/framework/utils.dart @@ -157,20 +157,28 @@ Future getFlutterRepoCommitTimestamp(String commit) { }); } -Future startProcess(String executable, List arguments, - {Map env}) async { +Future startProcess( + String executable, + List arguments, { + Map environment, + String workingDirectory, +}) async { String command = '$executable ${arguments?.join(" ") ?? ""}'; print('Executing: $command'); - Process proc = await Process.start(executable, arguments, - environment: env, workingDirectory: cwd); - ProcessInfo procInfo = new ProcessInfo(command, proc); - _runningProcesses.add(procInfo); + Process process = await Process.start( + executable, + arguments, + environment: environment, + workingDirectory: workingDirectory ?? cwd, + ); + ProcessInfo processInfo = new ProcessInfo(command, process); + _runningProcesses.add(processInfo); - proc.exitCode.whenComplete(() { - _runningProcesses.remove(procInfo); + process.exitCode.whenComplete(() { + _runningProcesses.remove(processInfo); }); - return proc; + return process; } Future forceQuitRunningProcesses() async { @@ -191,20 +199,24 @@ Future forceQuitRunningProcesses() async { } /// Executes a command and returns its exit code. -Future exec(String executable, List arguments, - {Map env, bool canFail: false}) async { - Process proc = await startProcess(executable, arguments, env: env); +Future exec( + String executable, + List arguments, { + Map environment, + bool canFail: false, +}) async { + Process process = await startProcess(executable, arguments, environment: environment); - proc.stdout + process.stdout .transform(UTF8.decoder) .transform(const LineSplitter()) .listen(print); - proc.stderr + process.stderr .transform(UTF8.decoder) .transform(const LineSplitter()) .listen(stderr.writeln); - int exitCode = await proc.exitCode; + int exitCode = await process.exitCode; if (exitCode != 0 && !canFail) fail('Executable failed with exit code $exitCode.'); @@ -215,14 +227,18 @@ Future exec(String executable, List arguments, /// Executes a command and returns its standard output as a String. /// /// Standard error is redirected to the current process' standard error stream. -Future eval(String executable, List arguments, - {Map env, bool canFail: false}) async { - Process proc = await startProcess(executable, arguments, env: env); - proc.stderr.listen((List data) { +Future eval( + String executable, + List arguments, { + Map environment, + bool canFail: false, +}) async { + Process process = await startProcess(executable, arguments, environment: environment); + process.stderr.listen((List data) { stderr.add(data); }); - String output = await UTF8.decodeStream(proc.stdout); - int exitCode = await proc.exitCode; + String output = await UTF8.decodeStream(process.stdout); + int exitCode = await process.exitCode; if (exitCode != 0 && !canFail) fail('Executable failed with exit code $exitCode.'); @@ -230,19 +246,25 @@ Future eval(String executable, List arguments, return output.trimRight(); } -Future flutter(String command, - {List options: const [], bool canFail: false, Map env}) { +Future flutter(String command, { + List options: const [], + bool canFail: false, + Map environment, +}) { List args = [command]..addAll(options); 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. -Future evalFlutter(String command, - {List options: const [], bool canFail: false, Map env}) { +Future evalFlutter(String command, { + List options: const [], + bool canFail: false, + Map environment, +}) { List args = [command]..addAll(options); return eval(path.join(flutterDirectory.path, 'bin', 'flutter'), args, - canFail: canFail, env: env); + canFail: canFail, environment: environment); } String get dartBin => diff --git a/dev/devicelab/lib/tasks/microbenchmarks.dart b/dev/devicelab/lib/tasks/microbenchmarks.dart index c9d47c65ddb..fea216d5d3d 100644 --- a/dev/devicelab/lib/tasks/microbenchmarks.dart +++ b/dev/devicelab/lib/tasks/microbenchmarks.dart @@ -47,9 +47,14 @@ TaskFunction createMicrobenchmarkTask() { }; } -Future _startFlutter({String command = 'run', List options: const [], bool canFail: false, Map env}) { +Future _startFlutter({ + String command = 'run', + List options: const [], + bool canFail: false, + Map environment, +}) { List args = ['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> _readJsonResults(Process process) { diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart index ace1efff7af..fb489370964 100644 --- a/dev/devicelab/lib/tasks/perf_tests.dart +++ b/dev/devicelab/lib/tasks/perf_tests.dart @@ -255,7 +255,7 @@ class MemoryTest { '-d', deviceId, '--use-existing-app', - ], env: { + ], environment: { 'VM_SERVICE_URL': 'http://localhost:$debugPort' }); diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml index 81e0dbab0f2..8c5c56b2a6d 100644 --- a/dev/devicelab/manifest.yaml +++ b/dev/devicelab/manifest.yaml @@ -68,6 +68,11 @@ tasks: stage: devicelab 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 diff --git a/dev/devicelab/test/adb_test.dart b/dev/devicelab/test/adb_test.dart index 2a1abf737a3..840758c3bc7 100644 --- a/dev/devicelab/test/adb_test.dart +++ b/dev/devicelab/test/adb_test.dart @@ -100,23 +100,29 @@ void expectLog(List log) { expect(FakeDevice.commandLog, log); } -CommandArgs cmd({String command, List arguments, Map env}) => new CommandArgs( - command: command, - arguments: arguments, - env: env -); +CommandArgs cmd({ + String command, + List arguments, + Map environment, +}) { + return new CommandArgs( + command: command, + arguments: arguments, + environment: environment, + ); +} typedef dynamic ExitErrorFactory(); class CommandArgs { - CommandArgs({this.command, this.arguments, this.env}); + CommandArgs({ this.command, this.arguments, this.environment }); final String command; final List arguments; - final Map env; + final Map environment; @override - String toString() => 'CommandArgs(command: $command, arguments: $arguments, env: $env)'; + String toString() => 'CommandArgs(command: $command, arguments: $arguments, environment: $environment)'; @override bool operator==(Object other) { @@ -126,18 +132,18 @@ class CommandArgs { CommandArgs otherCmd = other; return otherCmd.command == this.command && const ListEquality().equals(otherCmd.arguments, this.arguments) && - const MapEquality().equals(otherCmd.env, this.env); + const MapEquality().equals(otherCmd.environment, this.environment); } @override - int get hashCode => 17 * (17 * command.hashCode + _hashArguments) + _hashEnv; + int get hashCode => 17 * (17 * command.hashCode + _hashArguments) + _hashEnvironment; int get _hashArguments => arguments != null ? const ListEquality().hash(arguments) : null.hashCode; - int get _hashEnv => env != null - ? const MapEquality().hash(env) + int get _hashEnvironment => environment != null + ? const MapEquality().hash(environment) : null.hashCode; } @@ -166,21 +172,21 @@ class FakeDevice extends AndroidDevice { } @override - Future shellEval(String command, List arguments, {Map env}) async { + Future shellEval(String command, List arguments, { Map environment }) async { commandLog.add(new CommandArgs( command: command, arguments: arguments, - env: env + environment: environment, )); return output; } @override - Future shellExec(String command, List arguments, {Map env}) async { + Future shellExec(String command, List arguments, { Map environment }) async { commandLog.add(new CommandArgs( command: command, arguments: arguments, - env: env + environment: environment, )); dynamic exitError = exitErrorFactory(); if (exitError != null)