OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /// Compares the test log of a build step with previous builds. |
| 6 /// |
| 7 /// Use this to detect flakiness of failures, especially timeouts. |
| 8 |
| 9 import 'dart:async'; |
| 10 import 'dart:convert'; |
| 11 import 'dart:io'; |
| 12 |
| 13 main(List<String> args) async { |
| 14 if (args.length != 1) { |
| 15 print('Usage: compare_failures <log-uri>'); |
| 16 exit(1); |
| 17 } |
| 18 String url = args.first; |
| 19 if (!url.endsWith('/text')) { |
| 20 // Use the text version of the stdio log. |
| 21 url += '/text'; |
| 22 } |
| 23 Uri uri = Uri.parse(url); |
| 24 HttpClient client = new HttpClient(); |
| 25 BuildUri buildUri = new BuildUri(uri); |
| 26 List<BuildResult> results = await readBuildResults(client, buildUri); |
| 27 print(generateBuildResultsSummary(buildUri, results)); |
| 28 client.close(); |
| 29 } |
| 30 |
| 31 /// Creates a [BuildResult] for [buildUri] and, if it contains failures, the |
| 32 /// [BuildResult]s for the previous 5 builds. |
| 33 Future<List<BuildResult>> readBuildResults( |
| 34 HttpClient client, BuildUri buildUri) async { |
| 35 List<BuildResult> summaries = <BuildResult>[]; |
| 36 BuildResult firstSummary = await readBuildResult(client, buildUri); |
| 37 summaries.add(firstSummary); |
| 38 if (firstSummary.hasFailures) { |
| 39 for (int i = 0; i < 5; i++) { |
| 40 buildUri = buildUri.prev(); |
| 41 summaries.add(await readBuildResult(client, buildUri)); |
| 42 } |
| 43 } |
| 44 return summaries; |
| 45 } |
| 46 |
| 47 /// Reads the content of [uri] as text. |
| 48 Future<String> readUriAsText(HttpClient client, Uri uri) async { |
| 49 HttpClientRequest request = await client.getUrl(uri); |
| 50 HttpClientResponse response = await request.close(); |
| 51 return UTF8.decode(await response.expand((l) => l).toList()); |
| 52 } |
| 53 |
| 54 /// Parses the [buildUri] test log and creates a [BuildResult] for it. |
| 55 Future<BuildResult> readBuildResult( |
| 56 HttpClient client, BuildUri buildUri) async { |
| 57 Uri uri = buildUri.toUri(); |
| 58 log('Reading $uri'); |
| 59 String text = await readUriAsText(client, uri); |
| 60 |
| 61 bool inFailure = false; |
| 62 List<String> currentFailure; |
| 63 bool parsingTimingBlock = false; |
| 64 |
| 65 List<TestFailure> failures = <TestFailure>[]; |
| 66 List<Timing> timings = <Timing>[]; |
| 67 for (String line in text.split('\n')) { |
| 68 if (currentFailure != null) { |
| 69 if (line.startsWith('!@@@STEP_CLEAR@@@')) { |
| 70 failures.add(new TestFailure(buildUri, currentFailure)); |
| 71 currentFailure = null; |
| 72 } else { |
| 73 currentFailure.add(line); |
| 74 } |
| 75 } else if (inFailure && line.startsWith('@@@STEP_FAILURE@@@')) { |
| 76 inFailure = false; |
| 77 } else if (line.startsWith('!@@@STEP_FAILURE@@@')) { |
| 78 inFailure = true; |
| 79 } else if (line.startsWith('FAILED:')) { |
| 80 currentFailure = <String>[]; |
| 81 currentFailure.add(line); |
| 82 } |
| 83 if (line.startsWith('--- Total time:')) { |
| 84 parsingTimingBlock = true; |
| 85 } else if (parsingTimingBlock) { |
| 86 if (line.startsWith('0:')) { |
| 87 timings.addAll(parseTimings(buildUri, line)); |
| 88 } else { |
| 89 parsingTimingBlock = false; |
| 90 } |
| 91 } |
| 92 } |
| 93 return new BuildResult(buildUri, failures, timings); |
| 94 } |
| 95 |
| 96 /// Generate a summary of the timeouts and other failures in [results]. |
| 97 String generateBuildResultsSummary( |
| 98 BuildUri buildUri, List<BuildResult> results) { |
| 99 StringBuffer sb = new StringBuffer(); |
| 100 sb.write('Results for $buildUri:\n'); |
| 101 Set<TestConfiguration> timeoutIds = new Set<TestConfiguration>(); |
| 102 for (BuildResult result in results) { |
| 103 timeoutIds.addAll(result.timeouts.map((TestFailure failure) => failure.id)); |
| 104 } |
| 105 if (timeoutIds.isNotEmpty) { |
| 106 int firstBuildNumber = results.first.buildUri.buildNumber; |
| 107 int lastBuildNumber = results.last.buildUri.buildNumber; |
| 108 Map<TestConfiguration, Map<int, Map<String, Timing>>> map = |
| 109 <TestConfiguration, Map<int, Map<String, Timing>>>{}; |
| 110 Set<String> stepNames = new Set<String>(); |
| 111 for (BuildResult result in results) { |
| 112 for (Timing timing in result.timings) { |
| 113 Map<int, Map<String, Timing>> builds = |
| 114 map.putIfAbsent(timing.step.id, () => <int, Map<String, Timing>>{}); |
| 115 stepNames.add(timing.step.stepName); |
| 116 builds.putIfAbsent(timing.uri.buildNumber, () => <String, Timing>{})[ |
| 117 timing.step.stepName] = timing; |
| 118 } |
| 119 } |
| 120 sb.write('Timeouts for ${buildUri} :\n'); |
| 121 map.forEach((TestConfiguration id, Map<int, Map<String, Timing>> timings) { |
| 122 if (!timeoutIds.contains(id)) return; |
| 123 sb.write('$id\n'); |
| 124 sb.write( |
| 125 '${' ' * 8} ${stepNames.map((t) => padRight(t, 14)).join(' ')}\n'); |
| 126 for (int buildNumber = firstBuildNumber; |
| 127 buildNumber >= lastBuildNumber; |
| 128 buildNumber--) { |
| 129 Map<String, Timing> steps = timings[buildNumber] ?? const {}; |
| 130 sb.write(padRight(' ${buildNumber}: ', 8)); |
| 131 for (String stepName in stepNames) { |
| 132 Timing timing = steps[stepName]; |
| 133 if (timing != null) { |
| 134 sb.write(' ${timing.time}'); |
| 135 } else { |
| 136 sb.write(' --------------'); |
| 137 } |
| 138 } |
| 139 sb.write('\n'); |
| 140 } |
| 141 sb.write('\n'); |
| 142 }); |
| 143 } |
| 144 Set<TestConfiguration> errorIds = new Set<TestConfiguration>(); |
| 145 for (BuildResult result in results) { |
| 146 errorIds.addAll(result.errors.map((TestFailure failure) => failure.id)); |
| 147 } |
| 148 if (errorIds.isNotEmpty) { |
| 149 int firstBuildNumber = results.first.buildUri.buildNumber; |
| 150 int lastBuildNumber = results.last.buildUri.buildNumber; |
| 151 Map<TestConfiguration, Map<int, TestFailure>> map = <TestConfiguration, Map<
int, TestFailure>>{}; |
| 152 for (BuildResult result in results) { |
| 153 for (TestFailure failure in result.errors) { |
| 154 map.putIfAbsent(failure.id, () => <int, TestFailure>{})[ |
| 155 failure.uri.buildNumber] = failure; |
| 156 } |
| 157 } |
| 158 sb.write('Errors for ${buildUri} :\n'); |
| 159 // TODO(johnniwinther): Improve comparison of non-timeouts. |
| 160 map.forEach((TestConfiguration id, Map<int, TestFailure> failures) { |
| 161 if (!errorIds.contains(id)) return; |
| 162 sb.write('$id\n'); |
| 163 for (int buildNumber = firstBuildNumber; |
| 164 buildNumber >= lastBuildNumber; |
| 165 buildNumber--) { |
| 166 TestFailure failure = failures[buildNumber]; |
| 167 sb.write(padRight(' ${buildNumber}: ', 8)); |
| 168 if (failure != null) { |
| 169 sb.write(padRight(failure.expected, 10)); |
| 170 sb.write(' / '); |
| 171 sb.write(padRight(failure.actual, 10)); |
| 172 } else { |
| 173 sb.write(' ' * 10); |
| 174 sb.write(' / '); |
| 175 sb.write(padRight('-- OK --', 10)); |
| 176 } |
| 177 sb.write('\n'); |
| 178 } |
| 179 sb.write('\n'); |
| 180 }); |
| 181 } |
| 182 return sb.toString(); |
| 183 } |
| 184 |
| 185 /// The results of a build step. |
| 186 class BuildResult { |
| 187 final BuildUri buildUri; |
| 188 final List<TestFailure> _failures; |
| 189 final List<Timing> _timings; |
| 190 |
| 191 BuildResult(this.buildUri, this._failures, this._timings); |
| 192 |
| 193 /// `true` of the build result has test failures. |
| 194 bool get hasFailures => _failures.isNotEmpty; |
| 195 |
| 196 /// Returns the top-20 timings found in the build log. |
| 197 Iterable<Timing> get timings => _timings; |
| 198 |
| 199 /// Returns the [TestFailure]s for tests that timed out. |
| 200 Iterable<TestFailure> get timeouts { |
| 201 return _failures |
| 202 .where((TestFailure failure) => failure.actual == 'Timeout'); |
| 203 } |
| 204 |
| 205 /// Returns the [TestFailure]s for failing tests that did not time out. |
| 206 Iterable<TestFailure> get errors { |
| 207 return _failures |
| 208 .where((TestFailure failure) => failure.actual != 'Timeout'); |
| 209 } |
| 210 |
| 211 String toString() { |
| 212 StringBuffer sb = new StringBuffer(); |
| 213 sb.write('$buildUri\n'); |
| 214 sb.write('Failures:\n${_failures.join('\n-----\n')}\n'); |
| 215 sb.write('\nTimings:\n${_timings.join('\n')}'); |
| 216 return sb.toString(); |
| 217 } |
| 218 } |
| 219 |
| 220 /// The [Uri] of a build step stdio log split into its subparts. |
| 221 class BuildUri { |
| 222 final String scheme; |
| 223 final String host; |
| 224 final String prefix; |
| 225 final String botName; |
| 226 final int buildNumber; |
| 227 final String stepName; |
| 228 final String suffix; |
| 229 |
| 230 factory BuildUri(Uri uri) { |
| 231 String scheme = uri.scheme; |
| 232 String host = uri.host; |
| 233 List<String> parts = |
| 234 split(uri.path, ['/builders/', '/builds/', '/steps/', '/logs/']); |
| 235 String prefix = parts[0]; |
| 236 String botName = parts[1]; |
| 237 int buildNumber = int.parse(parts[2]); |
| 238 String stepName = parts[3]; |
| 239 String suffix = parts[4]; |
| 240 return new BuildUri.internal( |
| 241 scheme, host, prefix, botName, buildNumber, stepName, suffix); |
| 242 } |
| 243 |
| 244 BuildUri.internal(this.scheme, this.host, this.prefix, this.botName, |
| 245 this.buildNumber, this.stepName, this.suffix); |
| 246 |
| 247 String get buildName => |
| 248 '/builders/$botName/builds/$buildNumber/steps/$stepName'; |
| 249 |
| 250 String get path => '$prefix$buildName/logs/$suffix'; |
| 251 |
| 252 /// Creates the [Uri] for this build step stdio log. |
| 253 Uri toUri() { |
| 254 return new Uri( |
| 255 scheme: scheme, |
| 256 host: host, |
| 257 path: path); |
| 258 } |
| 259 |
| 260 /// Returns the [BuildUri] the previous build of this build step. |
| 261 BuildUri prev() { |
| 262 return new BuildUri.internal( |
| 263 scheme, host, prefix, botName, buildNumber - 1, stepName, suffix); |
| 264 } |
| 265 |
| 266 String toString() { |
| 267 return buildName; |
| 268 } |
| 269 } |
| 270 |
| 271 /// Id for a test on a specific configuration, for instance |
| 272 /// `dart2js-chrome release_x64/co19/Language/Metadata/before_function_t07`. |
| 273 class TestConfiguration { |
| 274 final String configName; |
| 275 final String testName; |
| 276 |
| 277 TestConfiguration(this.configName, this.testName); |
| 278 |
| 279 String toString() { |
| 280 return '$configName $testName'; |
| 281 } |
| 282 |
| 283 int get hashCode => configName.hashCode * 17 + testName.hashCode * 19; |
| 284 |
| 285 bool operator ==(other) { |
| 286 if (identical(this, other)) return true; |
| 287 if (other is! TestConfiguration) return false; |
| 288 return configName == other.configName && testName == other.testName; |
| 289 } |
| 290 } |
| 291 |
| 292 /// Test failure data derived from the test failure summary in the build step |
| 293 /// stdio log. |
| 294 class TestFailure { |
| 295 final BuildUri uri; |
| 296 final TestConfiguration id; |
| 297 final String expected; |
| 298 final String actual; |
| 299 final String text; |
| 300 |
| 301 factory TestFailure(BuildUri uri, List<String> lines) { |
| 302 List<String> parts = split(lines.first, ['FAILED: ', ' ', ' ']); |
| 303 String configName = parts[1]; |
| 304 String archName = parts[2]; |
| 305 String testName = parts[3]; |
| 306 TestConfiguration id = |
| 307 new TestConfiguration(configName, '$archName/$testName'); |
| 308 String expected = split(lines[1], ['Expected: '])[1]; |
| 309 String actual = split(lines[2], ['Actual: '])[1]; |
| 310 return new TestFailure.internal( |
| 311 uri, id, expected, actual, lines.skip(3).join('\n')); |
| 312 } |
| 313 |
| 314 TestFailure.internal( |
| 315 this.uri, this.id, this.expected, this.actual, this.text); |
| 316 |
| 317 String toString() { |
| 318 StringBuffer sb = new StringBuffer(); |
| 319 sb.write('FAILED: $id\n'); |
| 320 sb.write('Expected: $expected\n'); |
| 321 sb.write('Actual: $actual\n'); |
| 322 sb.write(text); |
| 323 return sb.toString(); |
| 324 } |
| 325 } |
| 326 |
| 327 /// Id for a single test step, for instance the compilation and run steps of |
| 328 /// a test. |
| 329 class TestStep { |
| 330 final String stepName; |
| 331 final TestConfiguration id; |
| 332 |
| 333 TestStep(this.stepName, this.id); |
| 334 |
| 335 String toString() { |
| 336 return '$stepName - $id'; |
| 337 } |
| 338 |
| 339 int get hashCode => stepName.hashCode * 13 + id.hashCode * 17; |
| 340 |
| 341 bool operator ==(other) { |
| 342 if (identical(this, other)) return true; |
| 343 if (other is! TestStep) return false; |
| 344 return stepName == other.stepName && id == other.id; |
| 345 } |
| 346 } |
| 347 |
| 348 /// The timing result for a single test step. |
| 349 class Timing { |
| 350 final BuildUri uri; |
| 351 final String time; |
| 352 final TestStep step; |
| 353 |
| 354 Timing(this.uri, this.time, this.step); |
| 355 |
| 356 String toString() { |
| 357 return '$time - $step'; |
| 358 } |
| 359 } |
| 360 |
| 361 /// Create the [Timing]s for the [line] as found in the top-20 timings of a |
| 362 /// build step stdio log. |
| 363 List<Timing> parseTimings(BuildUri uri, String line) { |
| 364 List<String> parts = split(line, [' - ', ' - ', ' ']); |
| 365 String time = parts[0]; |
| 366 String stepName = parts[1]; |
| 367 String configName = parts[2]; |
| 368 String testNames = parts[3]; |
| 369 List<Timing> timings = <Timing>[]; |
| 370 for (String testName in testNames.split(',')) { |
| 371 timings.add(new Timing(uri, time, |
| 372 new TestStep(stepName, |
| 373 new TestConfiguration(configName, testName.trim())))); |
| 374 } |
| 375 return timings; |
| 376 } |
| 377 |
| 378 /// Split [text] using [infixes] as infix markers. |
| 379 List<String> split(String text, List<String> infixes) { |
| 380 List<String> result = <String>[]; |
| 381 int start = 0; |
| 382 for (String infix in infixes) { |
| 383 int index = text.indexOf(infix, start); |
| 384 if (index == -1) |
| 385 throw "'$infix' not found in '$text' from offset ${start}."; |
| 386 result.add(text.substring(start, index)); |
| 387 start = index + infix.length; |
| 388 } |
| 389 result.add(text.substring(start)); |
| 390 return result; |
| 391 } |
| 392 |
| 393 /// Pad [text] with spaces to the right to fit [length]. |
| 394 String padRight(String text, int length) { |
| 395 if (text.length < length) return '${text}${' ' * (length - text.length)}'; |
| 396 return text; |
| 397 } |
| 398 |
| 399 void log(String text) { |
| 400 print(text); |
| 401 } |
OLD | NEW |