OLD | NEW |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 package analyzer | 5 package analyzer |
6 | 6 |
7 import ( | 7 import ( |
8 "fmt" | 8 "fmt" |
9 "strings" | 9 "strings" |
10 | 10 |
11 "infra/monitoring/client" | 11 "infra/monitoring/client" |
12 ) | 12 ) |
13 | 13 |
14 // TestFailureAnalyzer determines the reasons, if any, for a test step failure. | 14 // TestFailureAnalyzer determines the reasons, if any, for a test step failure. |
15 type TestFailureAnalyzer struct { | 15 type TestFailureAnalyzer struct { |
16 Client client.Client | 16 Client client.Client |
17 } | 17 } |
18 | 18 |
19 // Analyze returns the reasons, if any, for the step if it was a test failure. A
lso returns | 19 // Analyze returns the reasons, if any, for the step if it was a test failure. A
lso returns |
20 // whether or not the analyzer applied to the failure and any error in encounter
ed while | 20 // whether or not the analyzer applied to the failure and any error in encounter
ed while |
21 // running. | 21 // running. |
22 func (a *TestFailureAnalyzer) Analyze(f stepFailure) (*StepAnalyzerResult, error
) { | 22 func (a *TestFailureAnalyzer) Analyze(f stepFailure) (*StepAnalyzerResult, error
) { |
23 ret := &StepAnalyzerResult{} | 23 ret := &StepAnalyzerResult{} |
24 » if !strings.HasSuffix(f.step.Name, "tests") { | 24 » // Some test steps have names like "webkit_tests iOS(dbug)" so we look a
t the first |
| 25 » // term before the space, if there is one. |
| 26 » s := strings.Split(f.step.Name, " ") |
| 27 » if !strings.HasSuffix(s[0], "tests") { |
25 return ret, nil | 28 return ret, nil |
26 } | 29 } |
27 ret.Recognized = true | 30 ret.Recognized = true |
28 | 31 |
29 testResults, err := a.Client.TestResults(f.masterName, f.builderName, f.
step.Name, f.build.Number) | 32 testResults, err := a.Client.TestResults(f.masterName, f.builderName, f.
step.Name, f.build.Number) |
30 if err != nil { | 33 if err != nil { |
| 34 ret.Reasons = append(ret.Reasons, f.step.Name) |
31 return ret, fmt.Errorf("Error fetching test results: %v", err) | 35 return ret, fmt.Errorf("Error fetching test results: %v", err) |
32 } | 36 } |
33 | 37 |
34 if len(testResults.Tests) == 0 { | 38 if len(testResults.Tests) == 0 { |
35 » » log.Errorf("No test results for %v", f) | 39 » » return ret, fmt.Errorf("No test results for %v", f) |
36 } | 40 } |
37 log.Infof("%d test results", len(testResults.Tests)) | |
38 | 41 |
39 for testName, testResults := range testResults.Tests { | 42 for testName, testResults := range testResults.Tests { |
40 » » // This string splitting logic was copied from builder_alerts. | 43 » » res, ok := testResults.(map[string]interface{}) |
41 » » // I'm not sure it's really necessary since the strings appear t
o always | 44 » » if !ok { |
42 » » // be either "PASS" or "FAIL" in practice. | 45 » » » log.Errorf("Couldn't convert test results to map: %s", t
estName) |
43 » » expected := strings.Split(testResults.Expected, " ") | 46 » » » continue |
44 » » actual := strings.Split(testResults.Actual, " ") | 47 » » } |
45 » » ue := unexpected(expected, actual) | 48 |
| 49 » » // If res is a simple top-level test result, just check it here. |
| 50 » » if res["expected"] != nil || res["actual"] != nil { |
| 51 » » » expected := strings.Split(res["expected"].(string), " ") |
| 52 » » » actual := strings.Split(res["actual"].(string), " ") |
| 53 » » » ue := unexpected(expected, actual) |
| 54 » » » if len(ue) > 0 && res["bugs"] == nil { |
| 55 » » » » ret.Reasons = append(ret.Reasons, testName) |
| 56 » » » } |
| 57 » » » continue |
| 58 » » } |
| 59 |
| 60 » » // res is not a simple top-level test result, so recurse to find |
| 61 » » // the actual results. |
| 62 » » ue, err := traverseResults(testName, res) |
| 63 » » if err != nil { |
| 64 » » » return nil, err |
| 65 » » } |
46 if len(ue) > 0 { | 66 if len(ue) > 0 { |
| 67 // Don't give too many reasons, just the top level test/
suite names. |
| 68 // TODO: figure out a way to coalesce the unexpected res
ults list into |
| 69 // something compact but still meaningful in an alerting
context. |
47 ret.Reasons = append(ret.Reasons, testName) | 70 ret.Reasons = append(ret.Reasons, testName) |
48 } | 71 } |
49 } | 72 } |
50 return ret, nil | 73 return ret, nil |
51 } | 74 } |
| 75 |
| 76 // testResults json is an arbitrarily deep tree, whose nodes are the actual |
| 77 // test results, so we recurse to find them. |
| 78 func traverseResults(parent string, testResults map[string]interface{}) ([]strin
g, error) { |
| 79 ret := []string{} |
| 80 for testName, testResults := range testResults { |
| 81 res, ok := testResults.(map[string]interface{}) |
| 82 if !ok { |
| 83 return nil, fmt.Errorf("Couldn't convert test results to
map: %s/%s", parent, testName) |
| 84 } |
| 85 // First check if results is actually results, or just another b
ranch |
| 86 // in the tree of test results |
| 87 // Uuuuugly. |
| 88 if res["expected"] == nil || res["actual"] == nil { |
| 89 // Assume it's a branch. |
| 90 res, err := traverseResults(fmt.Sprintf("%s/%s", parent,
testName), res) |
| 91 if err != nil { |
| 92 return ret, err |
| 93 } |
| 94 ret = append(ret, res...) |
| 95 continue |
| 96 } |
| 97 |
| 98 expected := strings.Split(res["expected"].(string), " ") |
| 99 actual := strings.Split(res["actual"].(string), " ") |
| 100 ue := unexpected(expected, actual) |
| 101 if len(ue) > 0 && res["bugs"] == nil { |
| 102 ret = append(ret, fmt.Sprintf("%s/%s: %+v vs %+v", paren
t, testName, expected, actual)) |
| 103 } |
| 104 } |
| 105 return ret, nil |
| 106 } |
OLD | NEW |