1// Copyright 2020 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package build 16 17import ( 18 "errors" 19 "io/ioutil" 20 "os" 21 "path/filepath" 22 "reflect" 23 "sort" 24 "strconv" 25 "strings" 26 "testing" 27 "time" 28 29 "android/soong/ui/logger" 30) 31 32func writeBazelProfileFile(dir string) error { 33 contents := ` 34 35=== PHASE SUMMARY INFORMATION === 36 37Total launch phase time 1.193 s 15.77% 38Total init phase time 1.092 s 14.44% 39Total target pattern evaluation phase time 0.580 s 7.67% 40Total interleaved loading-and-analysis phase time 3.646 s 48.21% 41Total preparation phase time 0.022 s 0.30% 42Total execution phase time 0.993 s 13.13% 43Total finish phase time 0.036 s 0.48% 44--------------------------------------------------------------------- 45Total run time 7.563 s 100.00% 46 47Critical path (178 ms): 48 Time Percentage Description 49 178 ms 100.00% action 'BazelWorkspaceStatusAction stable-status.txt' 50 51` 52 file := filepath.Join(dir, "bazel_metrics.txt") 53 return os.WriteFile(file, []byte(contents), 0666) 54} 55 56func TestPruneMetricsFiles(t *testing.T) { 57 rootDir := t.TempDir() 58 59 dirs := []string{ 60 filepath.Join(rootDir, "d1"), 61 filepath.Join(rootDir, "d1", "d2"), 62 filepath.Join(rootDir, "d1", "d2", "d3"), 63 } 64 65 files := []string{ 66 filepath.Join(rootDir, "d1", "f1"), 67 filepath.Join(rootDir, "d1", "d2", "f1"), 68 filepath.Join(rootDir, "d1", "d2", "d3", "f1"), 69 } 70 71 for _, d := range dirs { 72 if err := os.MkdirAll(d, 0777); err != nil { 73 t.Fatalf("got %v, expecting nil error for making directory %q", err, d) 74 } 75 } 76 77 for _, f := range files { 78 if err := ioutil.WriteFile(f, []byte{}, 0777); err != nil { 79 t.Fatalf("got %v, expecting nil error on writing file %q", err, f) 80 } 81 } 82 83 want := []string{ 84 filepath.Join(rootDir, "d1", "f1"), 85 filepath.Join(rootDir, "d1", "d2", "f1"), 86 filepath.Join(rootDir, "d1", "d2", "d3", "f1"), 87 } 88 89 got := pruneMetricsFiles([]string{rootDir}) 90 91 sort.Strings(got) 92 sort.Strings(want) 93 94 if !reflect.DeepEqual(got, want) { 95 t.Errorf("got %q, want %q after pruning metrics files", got, want) 96 } 97} 98 99func TestUploadMetrics(t *testing.T) { 100 ctx := testContext() 101 tests := []struct { 102 description string 103 uploader string 104 createFiles bool 105 files []string 106 }{{ 107 description: "no metrics uploader", 108 }, { 109 description: "non-existent metrics files no upload", 110 uploader: "echo", 111 files: []string{"metrics_file_1", "metrics_file_2", "metrics_file_3, bazel_metrics.pb"}, 112 }, { 113 description: "trigger upload", 114 uploader: "echo", 115 createFiles: true, 116 files: []string{"metrics_file_1", "metrics_file_2, bazel_metrics.pb"}, 117 }} 118 119 for _, tt := range tests { 120 t.Run(tt.description, func(t *testing.T) { 121 defer logger.Recover(func(err error) { 122 t.Fatalf("got unexpected error: %v", err) 123 }) 124 125 outDir, err := ioutil.TempDir("", "") 126 if err != nil { 127 t.Fatalf("failed to create out directory: %v", outDir) 128 } 129 defer os.RemoveAll(outDir) 130 131 // Supply our own tmpDir to delete the temp dir once the test is done. 132 orgTmpDir := tmpDir 133 tmpDir = func(string, string) (string, error) { 134 retDir := filepath.Join(outDir, "tmp_upload_dir") 135 if err := os.Mkdir(retDir, 0755); err != nil { 136 t.Fatalf("failed to create temporary directory %q: %v", retDir, err) 137 } 138 return retDir, nil 139 } 140 defer func() { tmpDir = orgTmpDir }() 141 142 metricsUploadDir := filepath.Join(outDir, ".metrics_uploader") 143 if err := os.Mkdir(metricsUploadDir, 0755); err != nil { 144 t.Fatalf("failed to create %q directory for oauth valid check: %v", metricsUploadDir, err) 145 } 146 147 var metricsFiles []string 148 if tt.createFiles { 149 for _, f := range tt.files { 150 filename := filepath.Join(outDir, f) 151 metricsFiles = append(metricsFiles, filename) 152 if err := ioutil.WriteFile(filename, []byte("test file"), 0644); err != nil { 153 t.Fatalf("failed to create a fake metrics file %q for uploading: %v", filename, err) 154 } 155 } 156 } 157 if err := writeBazelProfileFile(outDir); err != nil { 158 t.Fatalf("failed to create bazel profile file in dir: %v", outDir) 159 } 160 161 config := Config{&configImpl{ 162 environ: &Environment{ 163 "OUT_DIR=" + outDir, 164 }, 165 buildDateTime: strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10), 166 metricsUploader: tt.uploader, 167 }} 168 169 UploadMetrics(ctx, config, false, time.Now(), "out/bazel_metrics.txt", "out/bazel_metrics.pb", metricsFiles...) 170 }) 171 } 172} 173 174func TestUploadMetricsErrors(t *testing.T) { 175 ctx := testContext() 176 tests := []struct { 177 description string 178 tmpDir string 179 tmpDirErr error 180 expectedErr string 181 }{{ 182 description: "getTmpDir returned error", 183 tmpDirErr: errors.New("getTmpDir failed"), 184 expectedErr: "getTmpDir failed", 185 }, { 186 description: "copyFile operation error", 187 tmpDir: "/fake_dir", 188 expectedErr: "failed to copy", 189 }} 190 191 for _, tt := range tests { 192 t.Run(tt.description, func(t *testing.T) { 193 defer logger.Recover(func(err error) { 194 got := err.Error() 195 if !strings.Contains(got, tt.expectedErr) { 196 t.Errorf("got %q, want %q to be contained in error", got, tt.expectedErr) 197 } 198 }) 199 200 outDir, err := ioutil.TempDir("", "") 201 if err != nil { 202 t.Fatalf("failed to create out directory: %v", outDir) 203 } 204 defer os.RemoveAll(outDir) 205 206 orgTmpDir := tmpDir 207 tmpDir = func(string, string) (string, error) { 208 return tt.tmpDir, tt.tmpDirErr 209 } 210 defer func() { tmpDir = orgTmpDir }() 211 212 metricsFile := filepath.Join(outDir, "metrics_file_1") 213 if err := ioutil.WriteFile(metricsFile, []byte("test file"), 0644); err != nil { 214 t.Fatalf("failed to create a fake metrics file %q for uploading: %v", metricsFile, err) 215 } 216 217 config := Config{&configImpl{ 218 environ: &Environment{ 219 "OUT_DIR=/bad", 220 }, 221 metricsUploader: "echo", 222 }} 223 224 UploadMetrics(ctx, config, true, time.Now(), "", "", metricsFile) 225 t.Errorf("got nil, expecting %q as a failure", tt.expectedErr) 226 }) 227 } 228} 229 230func TestParsePercentageToTenThousandths(t *testing.T) { 231 // 2.59% should be returned as 259 - representing 259/10000 of the build 232 percentage := parsePercentageToTenThousandths("2.59%") 233 if percentage != 259 { 234 t.Errorf("Expected percentage to be returned as ten-thousandths. Expected 259, have %d\n", percentage) 235 } 236 237 // Test without a leading digit 238 percentage = parsePercentageToTenThousandths(".52%") 239 if percentage != 52 { 240 t.Errorf("Expected percentage to be returned as ten-thousandths. Expected 52, have %d\n", percentage) 241 } 242} 243 244func TestParseTimingToNanos(t *testing.T) { 245 // This parses from seconds (with millis precision) and returns nanos 246 timingNanos := parseTimingToNanos("0.111") 247 if timingNanos != 111000000 { 248 t.Errorf("Error parsing timing. Expected 111000, have %d\n", timingNanos) 249 } 250 251 // Test without a leading digit 252 timingNanos = parseTimingToNanos(".112") 253 if timingNanos != 112000000 { 254 t.Errorf("Error parsing timing. Expected 112000, have %d\n", timingNanos) 255 } 256} 257 258func TestParsePhaseTiming(t *testing.T) { 259 // Sample lines include: 260 // Total launch phase time 0.011 s 2.59% 261 // Total target pattern evaluation phase time 0.012 s 4.59% 262 263 line1 := "Total launch phase time 0.011 s 2.59%" 264 timing := parsePhaseTiming(line1) 265 266 if timing.GetPhaseName() != "launch" { 267 t.Errorf("Failed to parse phase name. Expected launch, have %s\n", timing.GetPhaseName()) 268 } else if timing.GetDurationNanos() != 11000000 { 269 t.Errorf("Failed to parse duration nanos. Expected 11000000, have %d\n", timing.GetDurationNanos()) 270 } else if timing.GetPortionOfBuildTime() != 259 { 271 t.Errorf("Failed to parse portion of build time. Expected 259, have %d\n", timing.GetPortionOfBuildTime()) 272 } 273 274 // Test with a multiword phase name 275 line2 := "Total target pattern evaluation phase time 0.012 s 4.59%" 276 277 timing = parsePhaseTiming(line2) 278 if timing.GetPhaseName() != "target pattern evaluation" { 279 t.Errorf("Failed to parse phase name. Expected target pattern evaluation, have %s\n", timing.GetPhaseName()) 280 } else if timing.GetDurationNanos() != 12000000 { 281 t.Errorf("Failed to parse duration nanos. Expected 12000000, have %d\n", timing.GetDurationNanos()) 282 } else if timing.GetPortionOfBuildTime() != 459 { 283 t.Errorf("Failed to parse portion of build time. Expected 459, have %d\n", timing.GetPortionOfBuildTime()) 284 } 285} 286 287func TestParseTotal(t *testing.T) { 288 // Total line is in the form of: 289 // Total run time 7.563 s 100.00% 290 291 line := "Total run time 7.563 s 100.00%" 292 293 total := parseTotal(line) 294 295 // Only the seconds field is parsed, as nanos 296 if total != 7563000000 { 297 t.Errorf("Failed to parse total build time. Expected 7563000000, have %d\n", total) 298 } 299} 300